1#![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}