Skip to main content

feature_manifest/
lib.rs

1//! Library API for the `cargo-feature-manifest` and `cargo-fm` tools.
2//!
3//! The command-line interface and metadata format are the primary supported
4//! surfaces before `1.0`. The library API is available for integrations and
5//! tests, but may still change while the crate is pre-1.0.
6//!
7//! The crate reads Cargo feature definitions, attaches maintainer-authored
8//! metadata, validates common documentation and policy mistakes, and renders the
9//! result for docs or automation.
10//!
11//! Typical entry points:
12//!
13//! - [`load_workspace`] to discover workspace packages via `cargo metadata`.
14//! - [`load_manifest`] to read a single `Cargo.toml`.
15//! - [`validate`] to lint feature metadata.
16//! - [`render_markdown`] to generate documentation output.
17//! - [`render_mermaid`] to visualize feature relationships.
18//! - [`render_json`] to emit a versioned machine-readable schema.
19//! - [`sync_manifest`] to scaffold or normalize metadata tables.
20//! - [`preview_sync_manifest`] to inspect sync rewrites before writing.
21
22#![warn(missing_docs)]
23
24mod discover;
25mod docs_io;
26mod json_output;
27mod model;
28mod parse;
29mod render;
30mod source_map;
31mod validate;
32
33pub use discover::{PackageSelection, load_workspace, resolve_manifest_path};
34pub use docs_io::{
35    InjectionMarkers, InjectionReport, MarkerReport, ensure_injection_markers,
36    inject_between_markers, injected_region_matches, inspect_markers, output_matches, write_output,
37};
38pub use json_output::render_json;
39pub use model::{
40    DependencyInfo, Feature, FeatureGroup, FeatureManifest, FeatureMetadata, FeatureRef, LintLevel,
41    LintPreset, MetadataLayout, WorkspaceManifest,
42};
43pub use parse::{
44    FEATURE_DOCS_METADATA_TABLE, FEATURE_MANIFEST_METADATA_TABLE, SyncOptions, SyncPreview,
45    SyncReport, load_manifest, parse_manifest_str, preview_sync_manifest, render_sync_diff,
46    sync_manifest,
47};
48pub use render::{render_explain, render_markdown, render_mermaid};
49pub use source_map::{ManifestSourceMap, SourceSpan};
50pub use validate::{
51    Issue, KNOWN_LINT_CODES, LintDoc, Severity, ValidateOptions, ValidationReport,
52    known_lint_codes, lint_docs, parse_lint_override, validate, validate_with_options,
53};
54
55#[doc(hidden)]
56pub mod cli;
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    const SAMPLE_MANIFEST: &str = r#"
63[package]
64name = "demo"
65version = "0.1.0"
66
67[features]
68default = ["serde", "tokio?/rt"]
69serde = ["dep:serde"]
70tokio = ["dep:tokio", "std"]
71std = []
72unstable = []
73
74[package.metadata.feature-manifest]
75serde = { description = "Enable serde support." }
76tokio = { description = "Enable Tokio-backed APIs." }
77std = { description = "Enable std support." }
78unused = { description = "Not a real feature." }
79
80[package.metadata.feature-manifest.lints]
81small-group = "deny"
82
83[[package.metadata.feature-manifest.groups]]
84name = "runtime"
85members = ["tokio", "unstable"]
86mutually_exclusive = true
87"#;
88
89    #[test]
90    fn parses_typed_feature_references() {
91        let manifest = parse_manifest_str(SAMPLE_MANIFEST, "Cargo.toml").unwrap();
92        assert_eq!(manifest.package_name.as_deref(), Some("demo"));
93        assert_eq!(manifest.features.len(), 4);
94        assert_eq!(
95            manifest.default_members,
96            vec![
97                FeatureRef::Feature {
98                    name: "serde".to_owned()
99                },
100                FeatureRef::DependencyFeature {
101                    dependency: "tokio".to_owned(),
102                    feature: "rt".to_owned(),
103                    weak: true
104                }
105            ]
106        );
107        assert_eq!(
108            manifest.features["tokio"].enables,
109            vec![
110                FeatureRef::Dependency {
111                    name: "tokio".to_owned()
112                },
113                FeatureRef::Feature {
114                    name: "std".to_owned()
115                }
116            ]
117        );
118        assert_eq!(manifest.lint_overrides["small-group"], LintLevel::Deny);
119    }
120
121    #[test]
122    fn parses_structured_metadata_table() {
123        let manifest = parse_manifest_str(
124            r#"
125[package]
126name = "demo"
127version = "0.1.0"
128
129[features]
130cli = []
131
132[package.metadata.feature-manifest.features]
133cli = "Enable the CLI layer."
134"#,
135            "Cargo.toml",
136        )
137        .unwrap();
138
139        let cli = &manifest.features["cli"];
140        assert!(cli.has_metadata);
141        assert_eq!(
142            cli.metadata.description.as_deref(),
143            Some("Enable the CLI layer.")
144        );
145    }
146
147    #[test]
148    fn validation_reports_missing_and_stale_metadata() {
149        let manifest = parse_manifest_str(SAMPLE_MANIFEST, "Cargo.toml").unwrap();
150        let report = validate(&manifest);
151
152        assert!(report.has_errors());
153        assert!(
154            report
155                .issues
156                .iter()
157                .any(|issue| issue.code == "missing-metadata"
158                    && issue.feature.as_deref() == Some("unstable"))
159        );
160        assert!(
161            report
162                .issues
163                .iter()
164                .any(|issue| issue.code == "unknown-metadata"
165                    && issue.feature.as_deref() == Some("unused"))
166        );
167    }
168
169    #[test]
170    fn lint_overrides_can_downgrade_or_silence_issues() {
171        let manifest = parse_manifest_str(
172            r#"
173[package]
174name = "demo"
175version = "0.1.0"
176
177[features]
178alpha = []
179
180[package.metadata.feature-manifest]
181"#,
182            "Cargo.toml",
183        )
184        .unwrap();
185
186        let downgraded = validate_with_options(
187            &manifest,
188            &ValidateOptions::with_cli_lint_overrides([(
189                "missing-metadata".to_owned(),
190                LintLevel::Warn,
191            )]),
192        );
193        assert!(downgraded.warning_count() >= 1);
194        assert_eq!(downgraded.error_count(), 1);
195
196        let silenced = validate_with_options(
197            &manifest,
198            &ValidateOptions::with_cli_lint_overrides([
199                ("missing-metadata".to_owned(), LintLevel::Allow),
200                ("missing-description".to_owned(), LintLevel::Allow),
201            ]),
202        );
203        assert_eq!(silenced.issues.len(), 0);
204    }
205
206    #[test]
207    fn validation_reports_mutually_exclusive_default_conflicts() {
208        let manifest = parse_manifest_str(
209            r#"
210[package]
211name = "demo"
212version = "0.1.0"
213
214[features]
215default = ["native-tls", "rustls"]
216native-tls = []
217rustls = []
218
219[package.metadata.feature-manifest]
220native-tls = { description = "Use native-tls." }
221rustls = { description = "Use rustls." }
222
223[[package.metadata.feature-manifest.groups]]
224name = "tls"
225members = ["native-tls", "rustls"]
226mutually_exclusive = true
227"#,
228            "Cargo.toml",
229        )
230        .unwrap();
231
232        let report = validate(&manifest);
233        assert!(
234            report
235                .issues
236                .iter()
237                .any(|issue| issue.code == "mutually-exclusive-default")
238        );
239    }
240
241    #[test]
242    fn validation_allows_default_optional_dependency_features() {
243        let manifest = parse_manifest_str(
244            r#"
245[package]
246name = "demo"
247version = "0.1.0"
248
249[dependencies]
250serde = { version = "1", optional = true }
251
252[features]
253default = ["serde"]
254"#,
255            "Cargo.toml",
256        )
257        .unwrap();
258
259        let report = validate(&manifest);
260        assert!(
261            !report
262                .issues
263                .iter()
264                .any(|issue| issue.code == "unknown-default-member")
265        );
266    }
267
268    #[test]
269    fn validation_reports_unknown_plain_feature_references() {
270        let mut manifest = parse_manifest_str(
271            r#"
272[package]
273name = "demo"
274version = "0.1.0"
275
276[features]
277tls = ["native-tls"]
278
279[package.metadata.feature-manifest]
280tls = { description = "Enable TLS support." }
281"#,
282            "Cargo.toml",
283        )
284        .unwrap();
285        manifest.dependencies.insert(
286            "serde".to_owned(),
287            DependencyInfo {
288                key: "serde".to_owned(),
289                package: "serde".to_owned(),
290                optional: true,
291            },
292        );
293
294        let report = validate(&manifest);
295        assert!(report.issues.iter().any(|issue| {
296            issue.code == "unknown-feature-reference" && issue.feature.as_deref() == Some("tls")
297        }));
298    }
299
300    #[test]
301    fn validation_allows_plain_optional_dependency_references() {
302        let manifest = parse_manifest_str(
303            r#"
304[package]
305name = "demo"
306version = "0.1.0"
307
308[dependencies]
309native-tls = { version = "1", optional = true }
310
311[features]
312tls = ["native-tls"]
313
314[package.metadata.feature-manifest]
315tls = { description = "Enable TLS support." }
316"#,
317            "Cargo.toml",
318        )
319        .unwrap();
320
321        let report = validate(&manifest);
322        assert!(
323            !report
324                .issues
325                .iter()
326                .any(|issue| issue.code == "unknown-feature-reference")
327        );
328    }
329
330    #[test]
331    fn markdown_hides_private_features_by_default_and_shows_default_summary() {
332        let manifest = parse_manifest_str(
333            r#"
334[package]
335name = "demo"
336version = "0.1.0"
337
338[features]
339default = ["public-api"]
340public-api = []
341internal = []
342
343[package.metadata.feature-manifest]
344public-api = { description = "Stable public API surface." }
345internal = { description = "Internal glue.", public = false }
346"#,
347            "Cargo.toml",
348        )
349        .unwrap();
350
351        let workspace = WorkspaceManifest {
352            root_manifest_path: "Cargo.toml".into(),
353            packages: vec![manifest],
354        };
355        let markdown = render_markdown(&workspace, false);
356        assert!(markdown.contains("Default feature set: `public-api`"));
357        assert!(markdown.contains("public-api"));
358        assert!(!markdown.contains("| `internal` |"));
359        assert!(markdown.contains("internal/private feature(s) hidden"));
360    }
361
362    #[test]
363    fn source_map_finds_feature_metadata_and_group_spans() {
364        let source = r#"
365[features]
366serde = []
367"tls+rustls" = []
368
369[package.metadata.feature-manifest.features]
370serde = { description = "" }
371
372[[package.metadata.feature-manifest.groups]]
373name = "tls"
374members = ["tls+rustls"]
375"#;
376        let map = ManifestSourceMap::new(source);
377
378        assert_eq!(
379            map.feature_key_span("tls+rustls"),
380            Some(SourceSpan { line: 4, column: 1 })
381        );
382        assert_eq!(
383            map.metadata_key_span("serde"),
384            Some(SourceSpan { line: 7, column: 1 })
385        );
386        assert_eq!(
387            map.group_name_span("tls"),
388            Some(SourceSpan {
389                line: 10,
390                column: 1
391            })
392        );
393    }
394
395    #[test]
396    fn sync_diff_keeps_insertions_readable() {
397        let diff = render_sync_diff(
398            std::path::Path::new("Cargo.toml"),
399            "a\nb\nc\n",
400            "a\nb\nnew\nc\n",
401        );
402
403        assert!(diff.contains("\n b\n+new\n c\n"));
404        assert!(!diff.contains("-c\n+new"));
405    }
406
407    #[test]
408    fn lint_docs_match_known_codes() {
409        let documented = lint_docs().iter().map(|lint| lint.code).collect::<Vec<_>>();
410
411        assert_eq!(documented, known_lint_codes());
412    }
413}