hunyi/dsl/visibility.rs
1//! Visibility-boundary declaration DSL — [`VisibilityBoundary`] and its draft chain.
2
3use xuanji::Severity;
4
5use crate::rules::{VISIBILITY_MODULE_RULE, VISIBILITY_RULE, VISIBILITY_SUPER_RULE};
6
7/// The maximum declared visibility a governed module's direct items may carry. An item whose
8/// declared-visibility rank is strictly above the ceiling reacts; at or below it passes.
9/// (`Public` is deliberately not a ceiling — it would never react.)
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum VisibilityCeiling {
12 /// Allow up to `pub(crate)`; react on bare `pub`. The `must_not_declare_pub` case.
13 Crate,
14 /// Allow up to `pub(super)`; react on `pub` and `pub(crate)`.
15 Super,
16 /// Allow only module-private (private / `pub(self)`); react on any `pub`-family keyword.
17 Module,
18}
19
20impl VisibilityCeiling {
21 /// The ceiling's rank on the `pub`(3) > `pub(crate)`(2) > `pub(super)`(1) > private(0) scale;
22 /// an item reacts iff its own rank is strictly greater.
23 pub(crate) fn rank(self) -> u8 {
24 match self {
25 VisibilityCeiling::Crate => 2,
26 VisibilityCeiling::Super => 1,
27 VisibilityCeiling::Module => 0,
28 }
29 }
30
31 /// The rule label for this ceiling. `Crate` keeps the legacy `must_not_declare_pub` string
32 /// verbatim (so its findings and baselines never churn); `Super`/`Module` are distinct. Public
33 /// so the shell's projection renders the same label the reaction stamps (one source).
34 pub fn rule(self) -> &'static str {
35 match self {
36 VisibilityCeiling::Crate => VISIBILITY_RULE,
37 VisibilityCeiling::Super => VISIBILITY_SUPER_RULE,
38 VisibilityCeiling::Module => VISIBILITY_MODULE_RULE,
39 }
40 }
41}
42
43/// A visibility boundary: a governed module must not declare any bare-`pub` items —
44/// a declared-visibility hygiene rule for an internal / impl-detail layer. The rule is
45/// **syntactic** (the `pub` keyword on the module's own direct items), not crate-
46/// reachability: `pub(crate)`/`pub(super)`/`pub(in …)`/private are allowed, and attribute-
47/// derived public surface (`#[macro_export]`, `#[no_mangle]`) is out of scope (the deferred
48/// attribute capability's domain). Declared in Rust and composed with the other dimensions
49/// at the gate.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct VisibilityBoundary {
52 pub(crate) crate_package: String,
53 pub(crate) module: String,
54 pub(crate) reason: String,
55 pub(crate) anchor: Option<String>,
56 pub(crate) severity: Severity,
57 pub(crate) ceiling: VisibilityCeiling,
58}
59
60impl VisibilityBoundary {
61 /// Begin a visibility boundary in the crate named `package`.
62 pub fn in_crate(package: &str) -> VisibilityCrateDraft {
63 VisibilityCrateDraft {
64 crate_package: package.to_string(),
65 }
66 }
67
68 /// The crate this boundary governs.
69 pub fn crate_package(&self) -> &str {
70 &self.crate_package
71 }
72
73 /// The governed module path (e.g. `crate::internal`).
74 pub fn module(&self) -> &str {
75 &self.module
76 }
77
78 /// The human-readable reason recorded with the boundary (the repair hint).
79 pub fn reason(&self) -> &str {
80 &self.reason
81 }
82
83 /// Attach a durable governance anchor (e.g. `"ADR-014"`) — a stable pointer into the
84 /// project's governance, distinct from the free-text `reason`. Optional; a boundary with
85 /// none projects and reacts exactly as before.
86 pub fn with_anchor(mut self, anchor: &str) -> Self {
87 self.anchor = Some(anchor.to_string());
88 self
89 }
90
91 /// The durable governance anchor recorded with the boundary, if any.
92 pub fn anchor(&self) -> Option<&str> {
93 self.anchor.as_deref()
94 }
95
96 /// The boundary's severity (`enforce` or `warn`).
97 pub fn severity(&self) -> Severity {
98 self.severity
99 }
100
101 /// The boundary's maximum-visibility ceiling.
102 pub fn ceiling(&self) -> VisibilityCeiling {
103 self.ceiling
104 }
105}
106
107/// A visibility boundary awaiting its module anchor.
108#[doc(hidden)]
109pub struct VisibilityCrateDraft {
110 crate_package: String,
111}
112
113impl VisibilityCrateDraft {
114 /// Anchor the boundary to a module path within the crate (e.g. `crate::internal`).
115 pub fn module(self, module: &str) -> VisibilityModuleDraft {
116 VisibilityModuleDraft {
117 crate_package: self.crate_package,
118 module: module.to_string(),
119 }
120 }
121}
122
123/// A module-anchored boundary awaiting the rule.
124#[doc(hidden)]
125pub struct VisibilityModuleDraft {
126 crate_package: String,
127 module: String,
128}
129
130impl VisibilityModuleDraft {
131 /// Forbid the module from declaring any bare-`pub` direct item — sugar for
132 /// [`max_visibility`](Self::max_visibility)`(VisibilityCeiling::Crate)`, byte-identical in
133 /// behavior, rule string, and findings.
134 pub fn must_not_declare_pub(self) -> VisibilityBoundaryDraft {
135 self.max_visibility(VisibilityCeiling::Crate)
136 }
137
138 /// Forbid the module from declaring any direct item more visible than `ceiling`.
139 pub fn max_visibility(self, ceiling: VisibilityCeiling) -> VisibilityBoundaryDraft {
140 VisibilityBoundaryDraft {
141 crate_package: self.crate_package,
142 module: self.module,
143 severity: Severity::Enforce,
144 ceiling,
145 }
146 }
147}
148
149/// A boundary awaiting severity (optional) and its reason.
150#[doc(hidden)]
151pub struct VisibilityBoundaryDraft {
152 crate_package: String,
153 module: String,
154 severity: Severity,
155 ceiling: VisibilityCeiling,
156}
157
158impl VisibilityBoundaryDraft {
159 /// Make this an advisory (`warn`) boundary: violations are reported but do not fail the
160 /// reaction — the first rung of adoption.
161 pub fn warn(mut self) -> Self {
162 self.severity = Severity::Warn;
163 self
164 }
165
166 /// Finish the boundary with its human-readable reason (the repair hint).
167 pub fn because(self, reason: &str) -> VisibilityBoundary {
168 VisibilityBoundary {
169 crate_package: self.crate_package,
170 module: self.module,
171 reason: reason.to_string(),
172 anchor: None,
173 severity: self.severity,
174 ceiling: self.ceiling,
175 }
176 }
177}