Skip to main content

deps_core/
selection.rs

1//! Manifest-scoped state that can refine a registry's "latest matching" version selection.
2//!
3//! Beyond the plain requirement text — e.g. Composer's `minimum-stability` field (#424,
4//! #1433, #1444).
5//!
6//! [`SelectionContext`] used to carry Composer's `minimum-stability` as a raw
7//! `Option<String>`, with `Registry::get_latest_matching`/`select_latest_matching` each
8//! having a context-less trait method and a separate `_with_context` twin a caller had to
9//! remember to call instead. #1444 collapses that: [`crate::Registry::get_latest_matching`]
10//! and [`crate::Registry::select_latest_matching`] now take a required
11//! [`SelectionContext`] parameter directly (an explicit [`SelectionContext::none()`] is the
12//! only way to opt out, and that choice is now visible at every call site instead of an
13//! easy-to-forget separate method name), and the stability value itself is the typed
14//! [`StabilityFloor`] rather than a free-form string.
15use std::str::FromStr;
16
17/// A Composer `minimum-stability` floor (or per-dependency `@stability` flag), ranked
18/// `Dev < Alpha < Beta < Rc < Stable` (#424, #1444).
19///
20/// Declaration order is the precedence order: [`Ord`] compares two floors exactly as
21/// Composer's own stability scale does, so `StabilityFloor::Beta < StabilityFloor::Rc`
22/// holds via the derived comparison, with no separate rank table to keep in sync.
23///
24/// This is the *manifest-declared* stability floor's own type — strict, via
25/// [`FromStr`] — distinct from the lenient qualifier classification a *version string*'s
26/// own suffix (e.g. `1.0.0-beta.1`) goes through inside `deps-composer`'s
27/// `compare_versions`, which still accepts the `a`/`b` short aliases and maps an
28/// unrecognized word to [`Self::Stable`] — that leniency is unchanged by this type.
29///
30/// # Examples
31///
32/// ```
33/// use deps_core::selection::StabilityFloor;
34///
35/// assert!(StabilityFloor::Dev < StabilityFloor::Alpha);
36/// assert!(StabilityFloor::Alpha < StabilityFloor::Beta);
37/// assert!(StabilityFloor::Beta < StabilityFloor::Rc);
38/// assert!(StabilityFloor::Rc < StabilityFloor::Stable);
39/// ```
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
41pub enum StabilityFloor {
42    /// `dev` — the loosest floor; admits every stability level.
43    Dev,
44    /// `alpha`.
45    Alpha,
46    /// `beta`.
47    Beta,
48    /// `RC` (release candidate).
49    Rc,
50    /// `stable` — the strictest floor; Composer's own default.
51    Stable,
52}
53
54/// [`StabilityFloor::from_str`]'s error.
55///
56/// `raw` was not one of the five recognized stability keywords (`dev`, `alpha`, `beta`,
57/// `rc`, `stable`), matched ASCII-case-insensitively and with no surrounding-whitespace
58/// tolerance.
59///
60/// # Examples
61///
62/// ```
63/// use deps_core::selection::{StabilityFloor, UnknownStability};
64///
65/// assert_eq!("betta".parse::<StabilityFloor>(), Err(UnknownStability));
66/// assert_eq!(" beta".parse::<StabilityFloor>(), Err(UnknownStability));
67/// ```
68#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
69#[error("unrecognized stability keyword")]
70pub struct UnknownStability;
71
72impl FromStr for StabilityFloor {
73    type Err = UnknownStability;
74
75    /// Strict: accepts exactly `dev`, `alpha`, `beta`, `rc`, `stable`,
76    /// ASCII-case-insensitively (so `RC`, `Beta`, `STABLE` still parse) — no surrounding
77    /// whitespace tolerance, and no `a`/`b` short-alias acceptance (those are valid only
78    /// inside a *version qualifier* suffix, not as a `minimum-stability` config value or
79    /// `@flag`).
80    ///
81    /// This is the strict, config-value classifier; `deps-composer`'s own
82    /// `qualifier_stability` (private to that crate) is the deliberately separate, lenient
83    /// classifier for a version string's own qualifier suffix (accepts `a`/`b`, defaults an
84    /// unrecognized word to [`StabilityFloor::Stable`]) — if a keyword is ever added here,
85    /// check whether it belongs there too.
86    ///
87    /// # Examples
88    ///
89    /// ```
90    /// use deps_core::selection::StabilityFloor;
91    ///
92    /// assert_eq!("beta".parse(), Ok(StabilityFloor::Beta));
93    /// assert_eq!("RC".parse(), Ok(StabilityFloor::Rc));
94    /// assert_eq!("STABLE".parse(), Ok(StabilityFloor::Stable));
95    ///
96    /// assert!("a".parse::<StabilityFloor>().is_err());
97    /// assert!("b".parse::<StabilityFloor>().is_err());
98    /// assert!("".parse::<StabilityFloor>().is_err());
99    /// assert!(" beta".parse::<StabilityFloor>().is_err());
100    /// assert!("stable ".parse::<StabilityFloor>().is_err());
101    /// ```
102    fn from_str(s: &str) -> Result<Self, Self::Err> {
103        match s.to_ascii_lowercase().as_str() {
104            "dev" => Ok(Self::Dev),
105            "alpha" => Ok(Self::Alpha),
106            "beta" => Ok(Self::Beta),
107            "rc" => Ok(Self::Rc),
108            "stable" => Ok(Self::Stable),
109            _ => Err(UnknownStability),
110        }
111    }
112}
113
114/// One manifest-declared `minimum-stability` value that failed to parse.
115///
116/// Failed via [`StabilityFloor::from_str`] — returned by
117/// [`crate::ParseResult::invalid_minimum_stability`] and surfaced as a diagnostic by
118/// `deps_core::lsp_helpers::diagnostics`' shared notice.
119///
120/// # Examples
121///
122/// ```
123/// use deps_core::selection::InvalidStabilityOccurrence;
124/// use deps_core::position::{Position, Range};
125///
126/// let occurrence = InvalidStabilityOccurrence {
127///     range: Range::new(Position::new(0, 0), Position::new(0, 5)),
128///     raw: "betta".to_string(),
129/// };
130/// assert_eq!(occurrence.raw, "betta");
131/// ```
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct InvalidStabilityOccurrence {
134    /// Range of the offending value — the value's own span for a string that failed to
135    /// parse, or the declaring key's span for a non-string value.
136    pub range: crate::position::Range,
137    /// The string contents (for a string value that failed to parse) or the raw JSON
138    /// token text (for a non-string value).
139    pub raw: String,
140}
141
142/// Manifest-scoped state that can refine a registry's "latest matching" version selection
143/// beyond the plain requirement text — e.g. Composer's `minimum-stability` field (#424,
144/// #1433, #1444).
145///
146/// Opaque and ecosystem-owned: the type carries whatever an ecosystem's own [`crate::ParseResult`]
147/// puts into it (today, only Composer's `minimum-stability`), and only that ecosystem's own
148/// [`crate::Registry`] implementation reads it back — every other registry ignores it via
149/// [`crate::Registry::get_latest_matching`]/[`crate::Registry::select_latest_matching`]'s own
150/// default. Every LSP call site that needs "the latest version for this dependency" (hover,
151/// completion, code actions, and the background fetch path) obtains one from
152/// [`crate::ParseResult::selection_context`] and threads it into those two required-parameter
153/// trait methods, so a call site that already has a `ParseResult` in scope no longer has to
154/// independently remember to extract and thread the same manifest-level state by hand
155/// (#1433).
156///
157/// [`PartialEq`] is derived so a caller can detect *that* the context changed between two
158/// parses (e.g. `deps-lsp`'s edit-triggered refetch escalation) without needing to know what
159/// changed.
160///
161/// # Examples
162///
163/// ```
164/// use deps_core::selection::{SelectionContext, StabilityFloor};
165///
166/// let none = SelectionContext::none();
167/// assert_eq!(none.minimum_stability(), None);
168///
169/// let composer = SelectionContext::with_minimum_stability(StabilityFloor::Alpha);
170/// assert_eq!(composer.minimum_stability(), Some(StabilityFloor::Alpha));
171/// assert_ne!(none, composer);
172/// ```
173#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
174pub struct SelectionContext {
175    minimum_stability: Option<StabilityFloor>,
176}
177
178impl SelectionContext {
179    /// The empty context: every [`crate::Registry::get_latest_matching`]/
180    /// [`crate::Registry::select_latest_matching`] call degrades to plain requirement-text
181    /// matching. [`crate::ParseResult::selection_context`]'s default for every ecosystem with
182    /// no selection-context concept of its own.
183    ///
184    /// # Examples
185    ///
186    /// ```
187    /// use deps_core::selection::SelectionContext;
188    ///
189    /// assert_eq!(SelectionContext::none().minimum_stability(), None);
190    /// ```
191    #[must_use]
192    pub const fn none() -> Self {
193        Self {
194            minimum_stability: None,
195        }
196    }
197
198    /// Builds a context carrying a manifest's own `minimum-stability` floor
199    /// (`ComposerParseResult`'s `MinimumStability::Declared`, #424/#1433/#1444).
200    ///
201    /// # Examples
202    ///
203    /// ```
204    /// use deps_core::selection::{SelectionContext, StabilityFloor};
205    ///
206    /// let ctx = SelectionContext::with_minimum_stability(StabilityFloor::Beta);
207    /// assert_eq!(ctx.minimum_stability(), Some(StabilityFloor::Beta));
208    /// ```
209    #[must_use]
210    pub const fn with_minimum_stability(floor: StabilityFloor) -> Self {
211        Self {
212            minimum_stability: Some(floor),
213        }
214    }
215
216    /// The manifest's own stability floor, read by
217    /// [`crate::Registry::get_latest_matching`]/[`crate::Registry::select_latest_matching`]'s
218    /// one real implementor (`PackagistRegistry`) — every other registry ignores it via those
219    /// methods' own defaults.
220    ///
221    /// # Examples
222    ///
223    /// ```
224    /// use deps_core::selection::SelectionContext;
225    ///
226    /// assert_eq!(SelectionContext::none().minimum_stability(), None);
227    /// ```
228    #[must_use]
229    pub const fn minimum_stability(&self) -> Option<StabilityFloor> {
230        self.minimum_stability
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn stability_floor_from_str_accepts_all_five_words_case_insensitively() {
240        for (word, expected) in [
241            ("dev", StabilityFloor::Dev),
242            ("DEV", StabilityFloor::Dev),
243            ("alpha", StabilityFloor::Alpha),
244            ("Alpha", StabilityFloor::Alpha),
245            ("beta", StabilityFloor::Beta),
246            ("BETA", StabilityFloor::Beta),
247            ("rc", StabilityFloor::Rc),
248            ("RC", StabilityFloor::Rc),
249            ("stable", StabilityFloor::Stable),
250            ("Stable", StabilityFloor::Stable),
251        ] {
252            assert_eq!(
253                word.parse::<StabilityFloor>(),
254                Ok(expected),
255                "word: {word:?}"
256            );
257        }
258    }
259
260    #[test]
261    fn stability_floor_from_str_rejects_unrecognized_words() {
262        for word in ["a", "b", "", " beta", "stable ", "betta", "RC1"] {
263            assert!(
264                word.parse::<StabilityFloor>().is_err(),
265                "expected {word:?} to be rejected"
266            );
267        }
268    }
269
270    #[test]
271    fn stability_floor_ord_matches_composer_precedence() {
272        let mut floors = [
273            StabilityFloor::Stable,
274            StabilityFloor::Dev,
275            StabilityFloor::Rc,
276            StabilityFloor::Alpha,
277            StabilityFloor::Beta,
278        ];
279        floors.sort();
280        assert_eq!(
281            floors,
282            [
283                StabilityFloor::Dev,
284                StabilityFloor::Alpha,
285                StabilityFloor::Beta,
286                StabilityFloor::Rc,
287                StabilityFloor::Stable,
288            ]
289        );
290    }
291
292    #[test]
293    fn selection_context_none_has_no_minimum_stability() {
294        assert_eq!(SelectionContext::none().minimum_stability(), None);
295        assert_eq!(SelectionContext::default(), SelectionContext::none());
296    }
297
298    #[test]
299    fn selection_context_with_minimum_stability_roundtrips() {
300        let ctx = SelectionContext::with_minimum_stability(StabilityFloor::Rc);
301        assert_eq!(ctx.minimum_stability(), Some(StabilityFloor::Rc));
302        assert_ne!(ctx, SelectionContext::none());
303    }
304}