Skip to main content

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}