Skip to main content

feature_manifest/
model.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3use std::path::PathBuf;
4use std::str::FromStr;
5
6use anyhow::{Result, bail};
7use serde::{Deserialize, Serialize};
8
9/// A workspace-aware view of one or more Cargo packages selected for analysis.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct WorkspaceManifest {
12    /// Root workspace manifest used as the base for relative output paths.
13    pub root_manifest_path: PathBuf,
14    /// Selected package manifests in deterministic display order.
15    pub packages: Vec<FeatureManifest>,
16}
17
18impl WorkspaceManifest {
19    /// Returns `true` when exactly one package was selected.
20    pub fn is_single_package(&self) -> bool {
21        self.packages.len() == 1
22    }
23
24    /// Returns the selected package names in display order.
25    pub fn package_names(&self) -> Vec<&str> {
26        self.packages
27            .iter()
28            .filter_map(|package| package.package_name.as_deref())
29            .collect()
30    }
31}
32
33/// A normalized view of Cargo features plus structured feature metadata.
34#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
35pub struct FeatureManifest {
36    /// Path to the package manifest that was parsed.
37    pub manifest_path: PathBuf,
38    /// Cargo package name, when the manifest has a `[package]` section.
39    pub package_name: Option<String>,
40    /// Metadata table that was found, such as `feature-manifest`.
41    pub metadata_table: Option<String>,
42    /// Metadata layout used by the manifest.
43    pub metadata_layout: MetadataLayout,
44    /// Declared Cargo features keyed by feature name.
45    pub features: BTreeMap<String, Feature>,
46    /// Metadata entries that do not match a declared feature.
47    pub metadata_only: BTreeMap<String, FeatureMetadata>,
48    /// Raw typed members from the `default` feature set.
49    pub default_members: Vec<FeatureRef>,
50    /// Local feature names enabled by the `default` feature set.
51    pub default_features: BTreeSet<String>,
52    /// Feature groups declared in metadata.
53    pub groups: Vec<FeatureGroup>,
54    /// Dependencies known from the manifest or `cargo metadata`.
55    pub dependencies: BTreeMap<String, DependencyInfo>,
56    /// Manifest-defined lint level overrides.
57    pub lint_overrides: BTreeMap<String, LintLevel>,
58    /// Manifest-defined lint preset.
59    pub lint_preset: Option<LintPreset>,
60}
61
62impl FeatureManifest {
63    /// Returns the features in stable display order.
64    pub fn ordered_features(&self) -> Vec<&Feature> {
65        self.features.values().collect()
66    }
67
68    /// Returns the groups that contain the named feature.
69    pub fn groups_for_feature(&self, feature_name: &str) -> Vec<&FeatureGroup> {
70        self.groups
71            .iter()
72            .filter(|group| group.members.iter().any(|member| member == feature_name))
73            .collect()
74    }
75
76    /// Returns the features that directly reference the named feature.
77    pub fn reverse_dependencies(&self, feature_name: &str) -> Vec<&Feature> {
78        self.features
79            .values()
80            .filter(|feature| {
81                feature
82                    .enables
83                    .iter()
84                    .any(|reference| reference.local_feature_name() == Some(feature_name))
85            })
86            .collect()
87    }
88}
89
90/// A single Cargo feature and its associated metadata.
91#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
92pub struct Feature {
93    /// Cargo feature name.
94    pub name: String,
95    /// Metadata associated with the feature, or defaults if missing.
96    pub metadata: FeatureMetadata,
97    /// Whether the feature had an explicit metadata entry.
98    pub has_metadata: bool,
99    /// Feature references activated by this feature.
100    pub enables: Vec<FeatureRef>,
101    /// Whether this feature is included in the default feature set.
102    pub default_enabled: bool,
103}
104
105/// Dependency details relevant to feature validation.
106#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
107pub struct DependencyInfo {
108    /// Dependency key used in `Cargo.toml`.
109    pub key: String,
110    /// Underlying package name after rename handling.
111    pub package: String,
112    /// Whether Cargo marks the dependency as optional.
113    pub optional: bool,
114}
115
116/// Layout used for feature metadata inside `package.metadata`.
117#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
118#[serde(rename_all = "snake_case")]
119pub enum MetadataLayout {
120    /// Feature metadata is stored directly under `[package.metadata.feature-manifest]`.
121    Flat,
122    /// Feature metadata is stored under `[package.metadata.feature-manifest.features]`.
123    Structured,
124}
125
126impl fmt::Display for MetadataLayout {
127    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
128        match self {
129            Self::Flat => formatter.write_str("flat"),
130            Self::Structured => formatter.write_str("structured"),
131        }
132    }
133}
134
135impl FromStr for MetadataLayout {
136    type Err = anyhow::Error;
137
138    fn from_str(value: &str) -> Result<Self> {
139        match value {
140            "flat" => Ok(Self::Flat),
141            "structured" => Ok(Self::Structured),
142            _ => bail!("expected `flat` or `structured`, found `{value}`"),
143        }
144    }
145}
146
147/// Severity override policy for a specific lint code.
148#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
149#[serde(rename_all = "lowercase")]
150pub enum LintLevel {
151    /// Suppress the lint.
152    Allow,
153    /// Report the lint without failing validation.
154    Warn,
155    /// Report the lint as an error.
156    Deny,
157}
158
159impl fmt::Display for LintLevel {
160    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
161        match self {
162            Self::Allow => formatter.write_str("allow"),
163            Self::Warn => formatter.write_str("warn"),
164            Self::Deny => formatter.write_str("deny"),
165        }
166    }
167}
168
169impl FromStr for LintLevel {
170    type Err = anyhow::Error;
171
172    fn from_str(value: &str) -> Result<Self> {
173        match value {
174            "allow" => Ok(Self::Allow),
175            "warn" | "warning" => Ok(Self::Warn),
176            "deny" | "error" => Ok(Self::Deny),
177            _ => bail!("expected `allow`, `warn`, or `deny`, found `{value}`"),
178        }
179    }
180}
181
182/// A named lint policy intended to make adoption and strict CI setup easier.
183#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
184#[serde(rename_all = "lowercase")]
185pub enum LintPreset {
186    /// Downgrade common rollout issues while adopting metadata.
187    Adopt,
188    /// Treat subjective warnings as release-blocking errors.
189    Strict,
190}
191
192impl fmt::Display for LintPreset {
193    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
194        match self {
195            Self::Adopt => formatter.write_str("adopt"),
196            Self::Strict => formatter.write_str("strict"),
197        }
198    }
199}
200
201impl FromStr for LintPreset {
202    type Err = anyhow::Error;
203
204    fn from_str(value: &str) -> Result<Self> {
205        match value {
206            "adopt" => Ok(Self::Adopt),
207            "strict" => Ok(Self::Strict),
208            _ => bail!("expected `adopt` or `strict`, found `{value}`"),
209        }
210    }
211}
212
213/// A typed reference inside a feature definition.
214#[derive(Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord)]
215#[serde(tag = "kind", rename_all = "snake_case")]
216pub enum FeatureRef {
217    /// Reference to another local Cargo feature.
218    Feature {
219        /// Referenced feature name.
220        name: String,
221    },
222    /// Reference to an optional dependency using `dep:name`.
223    Dependency {
224        /// Dependency key.
225        name: String,
226    },
227    /// Reference to a dependency feature such as `tokio/rt`.
228    DependencyFeature {
229        /// Dependency key.
230        dependency: String,
231        /// Dependency feature name.
232        feature: String,
233        /// Whether the reference uses Cargo's weak `name?/feature` syntax.
234        weak: bool,
235    },
236    /// Reference syntax that could not be classified.
237    Unknown {
238        /// Original reference text.
239        raw: String,
240    },
241}
242
243impl FeatureRef {
244    /// Parses a raw Cargo feature entry into a typed reference.
245    pub fn parse(raw: &str) -> Self {
246        if let Some(name) = raw.strip_prefix("dep:") {
247            return Self::Dependency {
248                name: name.to_owned(),
249            };
250        }
251
252        if let Some((dependency, feature)) = raw.split_once("?/") {
253            return Self::DependencyFeature {
254                dependency: dependency.to_owned(),
255                feature: feature.to_owned(),
256                weak: true,
257            };
258        }
259
260        if let Some((dependency, feature)) = raw.split_once('/') {
261            return Self::DependencyFeature {
262                dependency: dependency.to_owned(),
263                feature: feature.to_owned(),
264                weak: false,
265            };
266        }
267
268        if raw.trim().is_empty() {
269            return Self::Unknown {
270                raw: raw.to_owned(),
271            };
272        }
273
274        Self::Feature {
275            name: raw.to_owned(),
276        }
277    }
278
279    /// Returns the local feature name when this is a local feature reference.
280    pub fn local_feature_name(&self) -> Option<&str> {
281        match self {
282            Self::Feature { name } => Some(name.as_str()),
283            _ => None,
284        }
285    }
286
287    /// Returns the dependency key for dependency-based references.
288    pub fn dependency_name(&self) -> Option<&str> {
289        match self {
290            Self::Dependency { name } => Some(name.as_str()),
291            Self::DependencyFeature { dependency, .. } => Some(dependency.as_str()),
292            _ => None,
293        }
294    }
295
296    /// Returns the reference in Cargo feature syntax.
297    pub fn raw(&self) -> String {
298        self.to_string()
299    }
300}
301
302impl fmt::Display for FeatureRef {
303    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
304        match self {
305            Self::Feature { name } => formatter.write_str(name),
306            Self::Dependency { name } => write!(formatter, "dep:{name}"),
307            Self::DependencyFeature {
308                dependency,
309                feature,
310                weak,
311            } => {
312                if *weak {
313                    write!(formatter, "{dependency}?/{feature}")
314                } else {
315                    write!(formatter, "{dependency}/{feature}")
316                }
317            }
318            Self::Unknown { raw } => formatter.write_str(raw),
319        }
320    }
321}
322
323/// A logical grouping of related features.
324#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
325#[serde(deny_unknown_fields)]
326pub struct FeatureGroup {
327    /// Group identifier shown in reports and generated output.
328    pub name: String,
329    /// Feature names that belong to the group.
330    pub members: Vec<String>,
331    #[serde(default)]
332    /// Whether multiple default-enabled members should be reported as an error.
333    pub mutually_exclusive: bool,
334    /// Optional group description shown in generated output.
335    pub description: Option<String>,
336}
337
338/// Additional author-provided metadata for a feature.
339#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
340#[serde(deny_unknown_fields)]
341pub struct FeatureMetadata {
342    /// Description shown in generated docs.
343    pub description: Option<String>,
344    /// Optional family label such as `runtime`, `tls`, or `serialization`.
345    pub category: Option<String>,
346    /// Version or release label where the feature became available.
347    pub since: Option<String>,
348    /// URL for feature-specific documentation.
349    pub docs: Option<String>,
350    /// URL for an issue tracking unstable or planned work.
351    pub tracking_issue: Option<String>,
352    #[serde(default)]
353    /// Prerequisite labels or related feature names.
354    pub requires: Vec<String>,
355    #[serde(default = "default_public")]
356    /// Whether the feature should appear in public generated output.
357    pub public: bool,
358    #[serde(default)]
359    /// Whether the feature is experimental.
360    pub unstable: bool,
361    #[serde(default)]
362    /// Whether the feature is deprecated.
363    pub deprecated: bool,
364    #[serde(default)]
365    /// Acknowledges intentional default enablement of private/deprecated/unstable features.
366    pub allow_default: bool,
367    /// Extra context appended to Markdown and explain output.
368    pub note: Option<String>,
369}
370
371impl Default for FeatureMetadata {
372    fn default() -> Self {
373        Self {
374            description: None,
375            category: None,
376            since: None,
377            docs: None,
378            tracking_issue: None,
379            requires: Vec::new(),
380            public: true,
381            unstable: false,
382            deprecated: false,
383            allow_default: false,
384            note: None,
385        }
386    }
387}
388
389impl FeatureMetadata {
390    /// Returns a human-readable list of status labels for display output.
391    pub fn status_labels(&self) -> Vec<&'static str> {
392        let mut labels = Vec::new();
393        if self.deprecated {
394            labels.push("deprecated");
395        }
396        if self.unstable {
397            labels.push("unstable");
398        }
399        if !self.public {
400            labels.push("private");
401        }
402        if labels.is_empty() {
403            labels.push("stable");
404        }
405        labels
406    }
407}
408
409fn default_public() -> bool {
410    true
411}