pluvy 0.0.1__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.
Files changed (153) hide show
  1. pluvy/__init__.py +1 -0
  2. pluvy/__main__.py +5 -0
  3. pluvy/analysis/__init__.py +1 -0
  4. pluvy/analysis/analyzer.py +115 -0
  5. pluvy/analysis/context.py +51 -0
  6. pluvy/analysis/contracts.py +69 -0
  7. pluvy/analysis/requirements.py +10 -0
  8. pluvy/analysis/scopes.py +53 -0
  9. pluvy/analysis/session.py +88 -0
  10. pluvy/architecture/__init__.py +17 -0
  11. pluvy/architecture/design.py +68 -0
  12. pluvy/architecture/design_contracts.py +15 -0
  13. pluvy/architecture/imports.py +27 -0
  14. pluvy/architecture/models.py +12 -0
  15. pluvy/architecture/options.py +109 -0
  16. pluvy/architecture/rules.py +98 -0
  17. pluvy/architecture.py +6 -0
  18. pluvy/cli.py +155 -0
  19. pluvy/diagnostics.py +72 -0
  20. pluvy/engine.py +21 -0
  21. pluvy/imports.py +21 -0
  22. pluvy/languages/__init__.py +1 -0
  23. pluvy/languages/alias_syntax.py +27 -0
  24. pluvy/languages/change_scopes.py +46 -0
  25. pluvy/languages/comments.py +44 -0
  26. pluvy/languages/design_syntax.py +64 -0
  27. pluvy/languages/encoding_syntax.py +93 -0
  28. pluvy/languages/go/__init__.py +5 -0
  29. pluvy/languages/go/consumed_fields.py +125 -0
  30. pluvy/languages/go/encoding.py +68 -0
  31. pluvy/languages/go/encoding_members.py +75 -0
  32. pluvy/languages/go/plugin.py +25 -0
  33. pluvy/languages/go/projection.py +27 -0
  34. pluvy/languages/go/rules.py +38 -0
  35. pluvy/languages/go/semantic_syntax.py +152 -0
  36. pluvy/languages/go/serialization_fields.py +156 -0
  37. pluvy/languages/go/syntax.py +21 -0
  38. pluvy/languages/go/test_syntax.py +82 -0
  39. pluvy/languages/go/value_origins.py +221 -0
  40. pluvy/languages/import_syntax.py +45 -0
  41. pluvy/languages/java/__init__.py +5 -0
  42. pluvy/languages/java/encoding.py +78 -0
  43. pluvy/languages/java/encoding_members.py +96 -0
  44. pluvy/languages/java/encoding_types.py +28 -0
  45. pluvy/languages/java/failure.py +49 -0
  46. pluvy/languages/java/plugin.py +19 -0
  47. pluvy/languages/java/projection.py +22 -0
  48. pluvy/languages/java/rules.py +59 -0
  49. pluvy/languages/java/semantic_syntax.py +251 -0
  50. pluvy/languages/java/syntax.py +112 -0
  51. pluvy/languages/java/test_syntax.py +149 -0
  52. pluvy/languages/java/type_syntax.py +103 -0
  53. pluvy/languages/javascript/__init__.py +5 -0
  54. pluvy/languages/javascript/encoding.py +63 -0
  55. pluvy/languages/javascript/encoding_members.py +82 -0
  56. pluvy/languages/javascript/failure.py +187 -0
  57. pluvy/languages/javascript/plugin.py +21 -0
  58. pluvy/languages/javascript/projection.py +98 -0
  59. pluvy/languages/javascript/semantic_syntax.py +353 -0
  60. pluvy/languages/javascript/syntax.py +55 -0
  61. pluvy/languages/javascript/test_syntax.py +258 -0
  62. pluvy/languages/metric_syntax.py +129 -0
  63. pluvy/languages/projection_syntax.py +211 -0
  64. pluvy/languages/python/__init__.py +5 -0
  65. pluvy/languages/python/encoding.py +24 -0
  66. pluvy/languages/python/encoding_members.py +93 -0
  67. pluvy/languages/python/failure.py +71 -0
  68. pluvy/languages/python/outputs.py +108 -0
  69. pluvy/languages/python/plugin.py +19 -0
  70. pluvy/languages/python/projection.py +78 -0
  71. pluvy/languages/python/qualified_names.py +12 -0
  72. pluvy/languages/python/result_syntax.py +146 -0
  73. pluvy/languages/python/rules.py +266 -0
  74. pluvy/languages/python/semantic_syntax.py +193 -0
  75. pluvy/languages/python/syntax.py +106 -0
  76. pluvy/languages/python/test_syntax.py +180 -0
  77. pluvy/languages/python/type_syntax.py +75 -0
  78. pluvy/languages/semantic_nodes.py +489 -0
  79. pluvy/languages/semantic_syntax.py +7 -0
  80. pluvy/languages/type_checks.py +45 -0
  81. pluvy/languages/typescript/__init__.py +5 -0
  82. pluvy/languages/typescript/contract.py +202 -0
  83. pluvy/languages/typescript/plugin.py +31 -0
  84. pluvy/languages/typescript/result_syntax.py +102 -0
  85. pluvy/languages/typescript/rules.py +45 -0
  86. pluvy/languages/typescript/semantic_syntax.py +84 -0
  87. pluvy/languages/typescript/syntax.py +15 -0
  88. pluvy/metrics/__init__.py +19 -0
  89. pluvy/metrics/contracts.py +31 -0
  90. pluvy/metrics/measure.py +48 -0
  91. pluvy/metrics/options.py +48 -0
  92. pluvy/metrics/rules.py +51 -0
  93. pluvy/models.py +20 -0
  94. pluvy/parsing/__init__.py +1 -0
  95. pluvy/parsing/treesitter.py +79 -0
  96. pluvy/plugins/__init__.py +1 -0
  97. pluvy/plugins/builtins.py +347 -0
  98. pluvy/plugins/bundles.py +70 -0
  99. pluvy/plugins/languages.py +49 -0
  100. pluvy/plugins/profiles.py +24 -0
  101. pluvy/plugins/registry.py +104 -0
  102. pluvy/policy/__init__.py +1 -0
  103. pluvy/policy/contracts.py +93 -0
  104. pluvy/policy/models.py +48 -0
  105. pluvy/policy/paths.py +8 -0
  106. pluvy/policy/type_options.py +80 -0
  107. pluvy/py.typed +0 -0
  108. pluvy/review/__init__.py +1 -0
  109. pluvy/review/acp.py +173 -0
  110. pluvy/review/channel.py +111 -0
  111. pluvy/review/contracts.py +56 -0
  112. pluvy/review/mcp_server.py +58 -0
  113. pluvy/review/prompt.py +33 -0
  114. pluvy/review/rules.py +33 -0
  115. pluvy/review/submission_models.py +72 -0
  116. pluvy/review/submissions.py +106 -0
  117. pluvy/review/wire.py +78 -0
  118. pluvy/semantics/__init__.py +1 -0
  119. pluvy/semantics/aliases.py +72 -0
  120. pluvy/semantics/boundaries.py +214 -0
  121. pluvy/semantics/encoding.py +121 -0
  122. pluvy/semantics/encoding_members.py +38 -0
  123. pluvy/semantics/encoding_options.py +81 -0
  124. pluvy/semantics/failure.py +116 -0
  125. pluvy/semantics/failure_options.py +36 -0
  126. pluvy/semantics/flow.py +63 -0
  127. pluvy/semantics/index.py +508 -0
  128. pluvy/semantics/model.py +181 -0
  129. pluvy/semantics/options.py +62 -0
  130. pluvy/semantics/overrides.py +164 -0
  131. pluvy/semantics/parameter_properties.py +98 -0
  132. pluvy/semantics/project.py +99 -0
  133. pluvy/semantics/projection/__init__.py +1 -0
  134. pluvy/semantics/projection/contracts.py +81 -0
  135. pluvy/semantics/projection/matching.py +129 -0
  136. pluvy/semantics/projection/options.py +34 -0
  137. pluvy/semantics/projection/rules.py +80 -0
  138. pluvy/semantics/results.py +86 -0
  139. pluvy/semantics/testing/__init__.py +1 -0
  140. pluvy/semantics/testing/contracts.py +54 -0
  141. pluvy/semantics/testing/gate.py +203 -0
  142. pluvy/semantics/testing/options.py +75 -0
  143. pluvy/semantics/testing/rules.py +111 -0
  144. pluvy/semantics/usage.py +123 -0
  145. pluvy/snapshots.py +370 -0
  146. pluvy/source.py +50 -0
  147. pluvy/suppressions.py +189 -0
  148. pluvy/syntax.py +16 -0
  149. pluvy-0.0.1.dist-info/METADATA +133 -0
  150. pluvy-0.0.1.dist-info/RECORD +153 -0
  151. pluvy-0.0.1.dist-info/WHEEL +4 -0
  152. pluvy-0.0.1.dist-info/entry_points.txt +2 -0
  153. pluvy-0.0.1.dist-info/licenses/LICENSE +21 -0
pluvy/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Parser-backed policy checks with concrete findings and immutable configuration."""
pluvy/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Run the installed architecture gate as a module."""
2
+
3
+ from pluvy.cli import main
4
+
5
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ """Language analyzer contracts and execution."""
@@ -0,0 +1,115 @@
1
+ """Language-independent execution over a parser's concrete context contract."""
2
+
3
+ import logging
4
+ from dataclasses import dataclass, field
5
+
6
+ from pluvy.analysis.context import TreeContext
7
+ from pluvy.analysis.contracts import Analyzer, ContextParser
8
+ from pluvy.analysis.scopes import AffectedScopes, ScopeSyntax
9
+ from pluvy.analysis.session import AnalysisSession, SemanticBinding
10
+ from pluvy.diagnostics import AnalysisError, Finding, SyntaxFailure
11
+ from pluvy.plugins.bundles import BundleRegistry
12
+ from pluvy.plugins.profiles import AnalysisProfile
13
+ from pluvy.policy.models import Policy
14
+ from pluvy.semantics.model import SemanticSyntax
15
+ from pluvy.source import LanguageDefinition, Source
16
+ from pluvy.suppressions import CommentSuppressions, DirectiveSyntax, ParsedSuppressions
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class TypedAnalyzer(Analyzer):
21
+ """Compose one context parser and its compatible registered rule objects."""
22
+
23
+ name: str = field()
24
+ definition: LanguageDefinition = field()
25
+ parser: ContextParser
26
+ bundles: BundleRegistry
27
+ profile: AnalysisProfile = field()
28
+ semantics: SemanticSyntax | None = None
29
+ api_version: int = 2
30
+ scopes: ScopeSyntax | None = None
31
+ directives: DirectiveSyntax | None = None
32
+
33
+ def __post_init__(self) -> None:
34
+ self.validate()
35
+
36
+ def validate(self) -> None:
37
+ """Validate late bundle additions independently of checker enable flags."""
38
+ self.profile.validate(self.rule_ids)
39
+ if self.bundles.requires_semantics and self.semantics is None:
40
+ raise AnalysisError("Required semantic capability is unavailable")
41
+
42
+ def needs_semantics(self, policy: Policy) -> bool:
43
+ """Select evidence work using enabled consumer requirements."""
44
+ return self.bundles.needs_semantics(policy.checkers.disabled_rules())
45
+
46
+ @property
47
+ def semantic_binding(self) -> SemanticBinding | None:
48
+ if self.semantics is None:
49
+ return None
50
+ return SemanticBinding(self.parser, self.semantics)
51
+
52
+ @property
53
+ def rule_ids(self) -> tuple[str, ...]:
54
+ return self.bundles.rule_ids
55
+
56
+ def analyze(
57
+ self, source: Source, policy: Policy, session: AnalysisSession | None = None
58
+ ) -> tuple[Finding, ...]:
59
+ self.validate()
60
+ if session is not None and session.policy != policy:
61
+ raise AnalysisError("Analysis session policy does not match this execution")
62
+ if source.language != self.definition.language:
63
+ raise AnalysisError(f"{self.name} cannot analyze {source.language}")
64
+ if self.profile.optional_unavailable:
65
+ logging.getLogger(__name__).warning(
66
+ "Optional analyses unavailable for %r: %s",
67
+ source.path,
68
+ ", ".join(self.profile.optional_unavailable),
69
+ )
70
+ disabled = policy.checkers.disabled_rules()
71
+ if all(rule in disabled for rule in self.rule_ids):
72
+ return ()
73
+ try:
74
+ context = self._context(source, policy, session)
75
+ except SyntaxFailure:
76
+ return ()
77
+ return self._findings(context, disabled)
78
+
79
+ def _findings(self, context: TreeContext, disabled: frozenset[str]) -> tuple[Finding, ...]:
80
+ findings = self.bundles.run(context, disabled)
81
+ suppressions = (
82
+ CommentSuppressions(self.directives, self.rule_ids).parse(context)
83
+ if self.directives is not None
84
+ else ParsedSuppressions()
85
+ )
86
+ lines: frozenset[int] | None = None
87
+ if context.source.changed_lines is not None:
88
+ if self.scopes is None:
89
+ raise AnalysisError(f"Affected-scope capability unavailable for {self.name}")
90
+ scopes = AffectedScopes(self.scopes)
91
+ lines = scopes.lines(context, suppressions.target_lines(context.source.changed_lines))
92
+ findings = tuple(finding for finding in findings if scopes.includes(finding, lines))
93
+ findings = suppressions.apply(findings, lines)
94
+ return tuple(
95
+ sorted(
96
+ (finding for finding in findings if finding.rule not in disabled), key=_finding_key
97
+ )
98
+ )
99
+
100
+ def _context(
101
+ self, source: Source, policy: Policy, session: AnalysisSession | None
102
+ ) -> TreeContext:
103
+ active = session or AnalysisSession(policy)
104
+ if session is None and self.semantics is not None and self.needs_semantics(policy):
105
+ active.prepare(
106
+ (source,), {source.language: SemanticBinding(self.parser, self.semantics)}
107
+ )
108
+ return active.context(source, self.parser)
109
+
110
+
111
+ def _finding_key(finding: Finding) -> tuple[str, int, int, str, str]:
112
+ return finding.path, finding.line, finding.column, finding.rule, finding.message
113
+
114
+
115
+ __all__ = ["SyntaxFailure", "TypedAnalyzer"]
@@ -0,0 +1,51 @@
1
+ """One tree-backed context and its session-owned semantic evidence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING
7
+
8
+ from tree_sitter import Node, Tree
9
+
10
+ from pluvy.diagnostics import AnalysisError
11
+ from pluvy.policy.models import Policy
12
+ from pluvy.source import Source
13
+
14
+ if TYPE_CHECKING:
15
+ from pluvy.semantics.boundaries import BoundaryEvidence
16
+ from pluvy.semantics.index import SemanticIndex
17
+ from pluvy.semantics.model import UsageEvidence
18
+
19
+
20
+ @dataclass
21
+ class TreeContext:
22
+ """Retain an exact source tree and evidence prepared before rule execution."""
23
+
24
+ source: Source
25
+ tree: Tree
26
+ policy: Policy
27
+ semantic_index: SemanticIndex | None = None
28
+ boundary_evidence: BoundaryEvidence | None = None
29
+ usage_evidence: UsageEvidence | None = None
30
+
31
+ @property
32
+ def root(self) -> Node:
33
+ return self.tree.root_node
34
+
35
+ def require_index(self) -> SemanticIndex:
36
+ """Reject rule composition without the required semantic capability."""
37
+ if self.semantic_index is None:
38
+ raise AnalysisError("Semantic evidence was not prepared for this rule")
39
+ return self.semantic_index
40
+
41
+ def require_boundary(self) -> BoundaryEvidence:
42
+ """Expose boundary evidence after its session-owned resolution phase."""
43
+ if self.boundary_evidence is None:
44
+ raise AnalysisError("Boundary evidence was not prepared for this rule")
45
+ return self.boundary_evidence
46
+
47
+ def require_usage(self) -> UsageEvidence:
48
+ """Expose prepared project-wide usage evidence to registered usage rules."""
49
+ if self.usage_evidence is None:
50
+ raise AnalysisError("Usage evidence was not prepared for this rule")
51
+ return self.usage_evidence
@@ -0,0 +1,69 @@
1
+ """Narrow tree-backed execution protocols independent of language identities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Protocol
6
+
7
+ from pluvy.analysis.context import TreeContext
8
+ from pluvy.analysis.requirements import RuleRequirements
9
+ from pluvy.diagnostics import Finding
10
+ from pluvy.plugins.profiles import AnalysisProfile
11
+ from pluvy.policy.models import Policy
12
+ from pluvy.source import LanguageDefinition, Source
13
+
14
+ if TYPE_CHECKING:
15
+ from pluvy.analysis.session import AnalysisSession, SemanticBinding
16
+
17
+
18
+ class Analyzer(Protocol):
19
+ """Analyze one registered grammar using application-owned requirements."""
20
+
21
+ @property
22
+ def name(self) -> str: ...
23
+
24
+ @property
25
+ def definition(self) -> LanguageDefinition: ...
26
+
27
+ @property
28
+ def profile(self) -> AnalysisProfile: ...
29
+
30
+ @property
31
+ def api_version(self) -> int: ...
32
+
33
+ @property
34
+ def rule_ids(self) -> tuple[str, ...]: ...
35
+
36
+ @property
37
+ def semantic_binding(self) -> SemanticBinding | None: ...
38
+
39
+ def needs_semantics(self, policy: Policy) -> bool: ...
40
+
41
+ def validate(self) -> None: ...
42
+
43
+ def analyze(
44
+ self, source: Source, policy: Policy, session: AnalysisSession | None = None
45
+ ) -> tuple[Finding, ...]: ...
46
+
47
+
48
+ class RuleBundle(Protocol):
49
+ """Own diagnostic identities and consume the same concrete syntax context."""
50
+
51
+ @property
52
+ def name(self) -> str: ...
53
+
54
+ @property
55
+ def api_version(self) -> int: ...
56
+
57
+ @property
58
+ def rule_ids(self) -> tuple[str, ...]: ...
59
+
60
+ @property
61
+ def requirements(self) -> RuleRequirements: ...
62
+
63
+ def check(self, context: TreeContext) -> tuple[Finding, ...]: ...
64
+
65
+
66
+ class ContextParser(Protocol):
67
+ """Own tree-sitter parsing, recovery and warning emission."""
68
+
69
+ def parse(self, source: Source, policy: Policy) -> TreeContext: ...
@@ -0,0 +1,10 @@
1
+ """Concrete capability requirements owned by rule execution."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class RuleRequirements:
8
+ """Declare whether a rule consumes resolved semantic and boundary evidence."""
9
+
10
+ semantic_evidence: bool = False
@@ -0,0 +1,53 @@
1
+ """Filter diagnostics by affected definitions without narrowing analysis evidence."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Protocol
5
+
6
+ from tree_sitter import Node
7
+
8
+ from pluvy.analysis.context import TreeContext
9
+ from pluvy.diagnostics import Finding
10
+ from pluvy.parsing.treesitter import walk
11
+
12
+
13
+ class ScopeSyntax(Protocol):
14
+ """Identify definition spans and the header of a class-like container."""
15
+
16
+ def definition(self, node: Node) -> bool: ...
17
+ def extent(self, node: Node) -> Node: ...
18
+ def header_end(self, node: Node) -> int | None: ...
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class AffectedScopes:
23
+ """Expand edited lines using grammar-owned definitions and module statements."""
24
+
25
+ syntax: ScopeSyntax
26
+
27
+ def lines(
28
+ self, context: TreeContext, additional: frozenset[int] = frozenset()
29
+ ) -> frozenset[int]:
30
+ touched = context.source.changed_lines
31
+ if touched is None:
32
+ return frozenset(range(1, context.root.end_point.row + 2))
33
+ touched = touched | additional
34
+ selected = set(touched)
35
+ for node in walk(context.root):
36
+ if self._affected(node, touched, context.root):
37
+ extent = self.syntax.extent(node)
38
+ selected.update(range(extent.start_point.row + 1, extent.end_point.row + 2))
39
+ return frozenset(selected)
40
+
41
+ def includes(self, finding: Finding, lines: frozenset[int]) -> bool:
42
+ """File placement and ownership remain obligations for every changed file."""
43
+ return finding.scope == "file" or finding.line in lines
44
+
45
+ def _affected(self, node: Node, touched: frozenset[int], root: Node) -> bool:
46
+ extent = self.syntax.extent(node)
47
+ start, end = extent.start_point.row + 1, extent.end_point.row + 1
48
+ header = self.syntax.header_end(node)
49
+ if header is not None:
50
+ end = header
51
+ elif not self.syntax.definition(node) and node.parent != root:
52
+ return False
53
+ return any(start <= line <= end for line in touched)
@@ -0,0 +1,88 @@
1
+ """Own exact snapshot trees and the single mutable semantic resolution phase."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from pluvy.analysis.context import TreeContext
6
+ from pluvy.analysis.contracts import ContextParser
7
+ from pluvy.diagnostics import AnalysisError, SyntaxFailure
8
+ from pluvy.policy.models import Policy
9
+ from pluvy.semantics.boundaries import BoundaryEvidence
10
+ from pluvy.semantics.index import SemanticIndex
11
+ from pluvy.semantics.model import SemanticSyntax
12
+ from pluvy.semantics.project import ProjectUsage
13
+ from pluvy.source import Language, Source
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class SemanticBinding:
18
+ """Bind a concrete parser and syntax capability at composition."""
19
+
20
+ parser: ContextParser
21
+ syntax: SemanticSyntax
22
+
23
+
24
+ class AnalysisSession:
25
+ """Scope parse caches and mutable evidence to one immutable policy execution."""
26
+
27
+ def __init__(self, policy: Policy) -> None:
28
+ self.policy = policy
29
+ self._contexts: dict[tuple[Source, int], TreeContext] = {}
30
+ self._project: ProjectUsage | None = None
31
+ self._rejected: dict[tuple[Source, int], SyntaxFailure] = {}
32
+
33
+ def context(self, source: Source, parser: ContextParser) -> TreeContext:
34
+ """Include parser identity and exact bytes so grammar or content cannot alias."""
35
+ key = source, id(parser)
36
+ if key in self._rejected:
37
+ raise self._rejected[key]
38
+ if key not in self._contexts:
39
+ self._contexts[key] = self._parse(source, parser)
40
+ return self._contexts[key]
41
+
42
+ def _parse(self, source: Source, parser: ContextParser) -> TreeContext:
43
+ try:
44
+ return parser.parse(source, self.policy)
45
+ except SyntaxFailure as error:
46
+ self._rejected[source, id(parser)] = error
47
+ for warning in error.warnings:
48
+ warning.emit()
49
+ raise
50
+ except AnalysisError:
51
+ raise
52
+ except Exception as error:
53
+ raise AnalysisError(f"Parser failed for {source.path}: {error}") from error
54
+
55
+ def prepare(
56
+ self, sources: tuple[Source, ...], bindings: dict[Language, SemanticBinding]
57
+ ) -> None:
58
+ """Resolve the complete snapshot set before exposing evidence to any rule."""
59
+ try:
60
+ self._prepare(sources, bindings)
61
+ except AnalysisError:
62
+ raise
63
+ except Exception as error:
64
+ raise AnalysisError(f"Semantic preparation failed: {error}") from error
65
+
66
+ def _prepare(
67
+ self, sources: tuple[Source, ...], bindings: dict[Language, SemanticBinding]
68
+ ) -> None:
69
+ indexes: list[SemanticIndex] = []
70
+ for source in dict.fromkeys(sources):
71
+ binding = bindings.get(source.language)
72
+ if binding is None:
73
+ continue
74
+ try:
75
+ context = self.context(source, binding.parser)
76
+ except SyntaxFailure:
77
+ continue
78
+ if context.semantic_index is None:
79
+ context.semantic_index = SemanticIndex(context, binding.syntax)
80
+ indexes.append(context.semantic_index)
81
+ complete = tuple(index for index in indexes if not index.context.root.has_error)
82
+ self._project = ProjectUsage(complete)
83
+ self._project.prepare()
84
+ for index in indexes:
85
+ index.context.boundary_evidence = BoundaryEvidence(index)
86
+ usage = self._project.evidence()
87
+ for index in indexes:
88
+ index.context.usage_evidence = usage
@@ -0,0 +1,17 @@
1
+ """Architecture contracts and dependency checks."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ __all__ = ["ArchitectureRule"]
6
+
7
+ if TYPE_CHECKING:
8
+ from pluvy.architecture.rules import ArchitectureRule
9
+
10
+
11
+ def __getattr__(name: str) -> type["ArchitectureRule"]:
12
+ """Load the public rule class without coupling option imports to execution."""
13
+ if name != "ArchitectureRule":
14
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
15
+ from pluvy.architecture.rules import ArchitectureRule
16
+
17
+ return ArchitectureRule
@@ -0,0 +1,68 @@
1
+ """Mechanical evidence for class-based extension ownership and polymorphic dispatch."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from tree_sitter import Node
6
+
7
+ from pluvy.analysis.context import TreeContext
8
+ from pluvy.analysis.requirements import RuleRequirements
9
+ from pluvy.architecture.design_contracts import DesignSyntax
10
+ from pluvy.architecture.options import DISPATCH, OBJECT_MODEL
11
+ from pluvy.diagnostics import Finding
12
+ from pluvy.parsing.treesitter import spelling, walk
13
+ from pluvy.policy.paths import matches
14
+ from pluvy.source import Source
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class ObjectDesignRule:
19
+ """Reject identity switches and public procedural entry points in configured modules."""
20
+
21
+ syntax: DesignSyntax
22
+ name: str = "builtin.object_design"
23
+ api_version: int = 2
24
+ requirements: RuleRequirements = RuleRequirements()
25
+ rule_ids: tuple[str, ...] = (DISPATCH.name, OBJECT_MODEL.name)
26
+
27
+ def check(self, context: TreeContext) -> tuple[Finding, ...]:
28
+ findings: list[Finding] = []
29
+ options = context.policy.checkers
30
+ for node in walk(context.root):
31
+ if options.architecture_dispatch.enabled and self.syntax.dispatch(node, context.source):
32
+ findings.append(
33
+ self._finding(
34
+ context.source,
35
+ node,
36
+ DISPATCH.name,
37
+ "Use a registered interface implementation instead of identity dispatch",
38
+ )
39
+ )
40
+ if options.architecture_object_model.enabled and matches(
41
+ context.source.path, options.architecture_object_model.paths
42
+ ):
43
+ findings.extend(self._procedural(context))
44
+ return tuple(findings)
45
+
46
+ def _procedural(self, context: TreeContext) -> tuple[Finding, ...]:
47
+ findings: list[Finding] = []
48
+ for function in self.syntax.module_functions(context.root):
49
+ name = self.syntax.function_name(function)
50
+ if name is not None and not spelling(name, context.source).startswith("_"):
51
+ findings.append(
52
+ self._finding(
53
+ context.source,
54
+ function,
55
+ OBJECT_MODEL.name,
56
+ "Own extension behavior in a class implementing a narrow interface",
57
+ )
58
+ )
59
+ return tuple(findings)
60
+
61
+ def _finding(self, source: Source, node: Node, rule: str, message: str) -> Finding:
62
+ return Finding(
63
+ path=source.path,
64
+ line=node.start_point.row + 1,
65
+ column=node.start_point.column + 1,
66
+ rule=rule,
67
+ message=message,
68
+ )
@@ -0,0 +1,15 @@
1
+ """Consumer-owned syntax evidence for architectural object-design checks."""
2
+
3
+ from typing import Protocol
4
+
5
+ from tree_sitter import Node
6
+
7
+ from pluvy.source import Source
8
+
9
+
10
+ class DesignSyntax(Protocol):
11
+ """Interpret selectors and public module functions without evaluating policy."""
12
+
13
+ def dispatch(self, node: Node, source: Source) -> bool: ...
14
+ def module_functions(self, root: Node) -> tuple[Node, ...]: ...
15
+ def function_name(self, node: Node) -> Node | None: ...
@@ -0,0 +1,27 @@
1
+ """Canonical extraction contracts and module identity resolution."""
2
+
3
+ from typing import Protocol
4
+
5
+ from pluvy.analysis.context import TreeContext
6
+ from pluvy.architecture.models import Import
7
+ from pluvy.source import Source
8
+
9
+
10
+ class ImportResolver(Protocol):
11
+ """Map a literal dependency to the names its language considers equivalent."""
12
+
13
+ def names(self, dependency: Import, source: Source) -> tuple[str, ...]: ...
14
+
15
+
16
+ class LiteralImportResolver(ImportResolver):
17
+ """Python and Go import specifiers are already module identities."""
18
+
19
+ def names(self, dependency: Import, source: Source) -> tuple[str, ...]:
20
+ """Keep the resolver signature; literal identities need no source path."""
21
+ return (dependency.module,)
22
+
23
+
24
+ class ImportExtractor(Protocol):
25
+ """Extract dependency evidence from one concrete parser context."""
26
+
27
+ def extract(self, context: TreeContext) -> tuple[Import, ...]: ...
@@ -0,0 +1,12 @@
1
+ """Syntactically visible dependency contracts."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class Import:
8
+ """A visible dependency at its source location."""
9
+
10
+ module: str
11
+ line: int
12
+ column: int
@@ -0,0 +1,109 @@
1
+ """Architecture checker contracts, options and registrations."""
2
+
3
+ from pydantic import Field
4
+
5
+ from pluvy.policy.contracts import (
6
+ CheckerConfiguration,
7
+ CheckerDefinition,
8
+ CheckerOptions,
9
+ CheckerState,
10
+ Contract,
11
+ )
12
+
13
+
14
+ class Boundary(Contract):
15
+ """Forbid literal or normalized imports from matching source paths."""
16
+
17
+ name: str = Field(min_length=1)
18
+ sources: tuple[str, ...] = Field(min_length=1)
19
+ forbidden_imports: tuple[str, ...] = Field(min_length=1)
20
+
21
+
22
+ class Owner(Contract):
23
+ """Assign a named responsibility to repository-relative paths."""
24
+
25
+ name: str = Field(min_length=1)
26
+ paths: tuple[str, ...] = Field(min_length=1)
27
+
28
+
29
+ class SourcePlacement(Contract):
30
+ """Keep explicitly selected source modules within their owning locations."""
31
+
32
+ name: str = Field(min_length=1)
33
+ paths: tuple[str, ...] = Field(min_length=1)
34
+ allowed_paths: tuple[str, ...] = Field(min_length=1)
35
+
36
+
37
+ class StructureOptions(CheckerOptions):
38
+ """Configure source placement independently of ownership and dependencies."""
39
+
40
+ placements: tuple[SourcePlacement, ...] = ()
41
+
42
+
43
+ class OwnershipOptions(CheckerOptions):
44
+ """Configure responsibility matching only for architecture.owner."""
45
+
46
+ owners: tuple[Owner, ...] = ()
47
+
48
+
49
+ class ImportOptions(CheckerOptions):
50
+ """Configure forbidden dependencies only for architecture.import."""
51
+
52
+ boundaries: tuple[Boundary, ...] = ()
53
+
54
+
55
+ class ObjectModelOptions(CheckerOptions):
56
+ """Select modules whose public extension behavior must belong to objects."""
57
+
58
+ paths: tuple[str, ...] = ()
59
+
60
+
61
+ OWNERSHIP = CheckerDefinition("architecture.owner")
62
+
63
+ STRUCTURE = CheckerDefinition("architecture.structure")
64
+
65
+ IMPORTS = CheckerDefinition("architecture.import")
66
+
67
+ DISPATCH = CheckerDefinition("architecture.dispatch")
68
+
69
+ OBJECT_MODEL = CheckerDefinition("architecture.object_model")
70
+
71
+
72
+ class ArchitectureCheckers(CheckerConfiguration):
73
+ """Register owned checker fields and project their execution switches."""
74
+
75
+ architecture_owner: OwnershipOptions = Field(
76
+ default=OwnershipOptions(),
77
+ validation_alias=OWNERSHIP.name,
78
+ serialization_alias=OWNERSHIP.name,
79
+ )
80
+ architecture_structure: StructureOptions = Field(
81
+ default=StructureOptions(),
82
+ validation_alias=STRUCTURE.name,
83
+ serialization_alias=STRUCTURE.name,
84
+ )
85
+ architecture_import: ImportOptions = Field(
86
+ default=ImportOptions(),
87
+ validation_alias=IMPORTS.name,
88
+ serialization_alias=IMPORTS.name,
89
+ )
90
+ architecture_dispatch: CheckerOptions = Field(
91
+ default=CheckerOptions(),
92
+ validation_alias=DISPATCH.name,
93
+ serialization_alias=DISPATCH.name,
94
+ )
95
+ architecture_object_model: ObjectModelOptions = Field(
96
+ default=ObjectModelOptions(),
97
+ validation_alias=OBJECT_MODEL.name,
98
+ serialization_alias=OBJECT_MODEL.name,
99
+ )
100
+
101
+ def checker_states(self) -> tuple[CheckerState, ...]:
102
+ return (
103
+ *super().checker_states(),
104
+ OWNERSHIP.configure(self.architecture_owner.enabled),
105
+ STRUCTURE.configure(self.architecture_structure.enabled),
106
+ IMPORTS.configure(self.architecture_import.enabled),
107
+ DISPATCH.configure(self.architecture_dispatch.enabled),
108
+ OBJECT_MODEL.configure(self.architecture_object_model.enabled),
109
+ )