1use std::fmt;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum LifecycleStage {
34 Experimental,
36 #[default]
38 Stable,
39 Superseded,
41 SoftDeprecated,
43 Deprecated,
45 Defunct,
47}
48
49impl LifecycleStage {
50 pub fn from_str(s: &str) -> Option<Self> {
55 match s.to_lowercase().as_str() {
56 "experimental" => Some(Self::Experimental),
57 "stable" => Some(Self::Stable),
58 "superseded" => Some(Self::Superseded),
59 "soft-deprecated" | "soft_deprecated" => Some(Self::SoftDeprecated),
60 "deprecated" => Some(Self::Deprecated),
61 "defunct" => Some(Self::Defunct),
62 _ => None,
63 }
64 }
65
66 pub fn badge(&self) -> Option<&'static str> {
72 match self {
73 Self::Experimental => Some(r#"`r lifecycle::badge("experimental")`"#),
74 Self::Stable => None, Self::Superseded => Some(r#"`r lifecycle::badge("superseded")`"#),
76 Self::SoftDeprecated | Self::Deprecated => {
77 Some(r#"`r lifecycle::badge("deprecated")`"#)
78 }
79 Self::Defunct => Some(r#"`r lifecycle::badge("deprecated")`"#),
80 }
81 }
82
83 pub fn signal_fn(&self) -> Option<&'static str> {
89 match self {
90 Self::Experimental => Some("lifecycle::signal_stage"),
91 Self::Stable => None,
92 Self::Superseded => Some("lifecycle::signal_stage"),
93 Self::SoftDeprecated => Some("lifecycle::deprecate_soft"),
94 Self::Deprecated => Some("lifecycle::deprecate_warn"),
95 Self::Defunct => Some("lifecycle::deprecate_stop"),
96 }
97 }
98
99 pub fn import_from_fn(&self) -> Option<&'static str> {
103 match self {
104 Self::Experimental | Self::Superseded => Some("signal_stage"),
105 Self::Stable => None,
106 Self::SoftDeprecated => Some("deprecate_soft"),
107 Self::Deprecated => Some("deprecate_warn"),
108 Self::Defunct => Some("deprecate_stop"),
109 }
110 }
111
112 pub fn keywords(&self) -> Option<&'static str> {
117 match self {
118 Self::Experimental => Some("internal"),
119 Self::Stable => None,
120 Self::Superseded => None,
121 Self::SoftDeprecated | Self::Deprecated | Self::Defunct => None,
122 }
123 }
124}
125
126impl fmt::Display for LifecycleStage {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 match self {
129 Self::Experimental => write!(f, "experimental"),
130 Self::Stable => write!(f, "stable"),
131 Self::Superseded => write!(f, "superseded"),
132 Self::SoftDeprecated => write!(f, "soft-deprecated"),
133 Self::Deprecated => write!(f, "deprecated"),
134 Self::Defunct => write!(f, "defunct"),
135 }
136 }
137}
138
139#[derive(Debug, Clone, Default)]
141pub struct LifecycleSpec {
142 pub stage: LifecycleStage,
144 pub when: Option<String>,
146 pub what: Option<String>,
149 pub with: Option<String>,
151 pub details: Option<String>,
153 pub id: Option<String>,
155}
156
157impl LifecycleSpec {
158 pub fn new(stage: LifecycleStage) -> Self {
160 Self {
161 stage,
162 ..Default::default()
163 }
164 }
165
166 pub fn from_deprecated(since: Option<&str>, note: Option<&str>) -> Self {
172 let mut spec = Self::new(LifecycleStage::Deprecated);
173 spec.when = since.map(String::from);
174
175 if let Some(note) = note {
177 if let Some(rest) = note.to_lowercase().strip_prefix("use ") {
178 if let Some(end) = rest.find(" instead") {
179 spec.with = Some(rest[..end].to_string());
180 } else {
181 spec.with = Some(rest.to_string());
182 }
183 }
184 spec.details = Some(note.to_string());
185 }
186
187 spec
188 }
189
190 pub fn r_prelude(&self, fn_name: &str) -> Option<String> {
199 let signal_fn = self.stage.signal_fn()?;
200
201 let what = self.what.as_deref().unwrap_or(fn_name);
202
203 match self.stage {
204 LifecycleStage::Experimental | LifecycleStage::Superseded => {
205 Some(format!("{}(\"{}\", \"{}()\")", signal_fn, self.stage, what))
207 }
208 LifecycleStage::SoftDeprecated
209 | LifecycleStage::Deprecated
210 | LifecycleStage::Defunct => {
211 let when = self.when.as_deref().unwrap_or("0.0.0");
213 let what_arg = format!("\"{}()\"", what);
214 let with_arg = self
215 .with
216 .as_ref()
217 .map(|w| format!(", \"{}\"", w))
218 .unwrap_or_default();
219 let details_arg = self
220 .details
221 .as_ref()
222 .map(|d| format!(", details = \"{}\"", d.replace('"', "\\\"")))
223 .unwrap_or_default();
224 let id_arg = self
225 .id
226 .as_ref()
227 .map(|id| format!(", id = \"{}\"", id))
228 .unwrap_or_default();
229
230 Some(format!(
231 "{}(\"{}\", {}{}{}{})",
232 signal_fn, when, what_arg, with_arg, details_arg, id_arg
233 ))
234 }
235 LifecycleStage::Stable => None,
236 }
237 }
238}
239
240pub fn collect_lifecycle_imports<'a>(
246 specs: impl Iterator<Item = &'a LifecycleSpec>,
247) -> Option<String> {
248 let mut fns = std::collections::BTreeSet::new();
249 for spec in specs {
250 if spec.stage.badge().is_some() {
251 fns.insert("badge");
252 }
253 if let Some(fn_name) = spec.stage.import_from_fn() {
254 fns.insert(fn_name);
255 }
256 }
257 if fns.is_empty() {
258 None
259 } else {
260 let mut import = String::from("@importFrom lifecycle");
261 for f in fns {
262 import.push(' ');
263 import.push_str(f);
264 }
265 Some(import)
266 }
267}
268
269pub fn parse_lifecycle_attr(meta: &syn::Meta) -> syn::Result<Option<LifecycleSpec>> {
275 use syn::spanned::Spanned;
276
277 match meta {
278 syn::Meta::NameValue(nv) if nv.path.is_ident("lifecycle") => {
279 if let syn::Expr::Lit(syn::ExprLit {
281 lit: syn::Lit::Str(lit),
282 ..
283 }) = &nv.value
284 {
285 let stage = LifecycleStage::from_str(&lit.value()).ok_or_else(|| {
286 syn::Error::new_spanned(
287 lit,
288 "invalid lifecycle stage; expected one of: experimental, stable, superseded, soft-deprecated, deprecated, defunct",
289 )
290 })?;
291 return Ok(Some(LifecycleSpec::new(stage)));
292 }
293 Err(syn::Error::new_spanned(
294 &nv.value,
295 "lifecycle expects a string literal",
296 ))
297 }
298 syn::Meta::List(list) if list.path.is_ident("lifecycle") => {
299 let mut spec = LifecycleSpec::default();
301
302 let nested: syn::punctuated::Punctuated<syn::Meta, syn::Token![,]> =
303 list.parse_args_with(syn::punctuated::Punctuated::parse_terminated)?;
304
305 for meta in nested {
306 if let syn::Meta::NameValue(nv) = meta {
307 let key = nv.path.get_ident().map(|i| i.to_string());
308 let value = match &nv.value {
309 syn::Expr::Lit(expr_lit) => {
310 if let syn::Lit::Str(lit) = &expr_lit.lit {
311 lit.value()
312 } else {
313 return Err(syn::Error::new_spanned(
314 &nv.value,
315 "expected string literal",
316 ));
317 }
318 }
319 _ => {
320 return Err(syn::Error::new_spanned(
321 &nv.value,
322 "expected string literal",
323 ));
324 }
325 };
326
327 match key.as_deref() {
328 Some("stage") => {
329 spec.stage = LifecycleStage::from_str(&value).ok_or_else(|| {
330 syn::Error::new(nv.value.span(), "invalid lifecycle stage")
331 })?;
332 }
333 Some("when") => spec.when = Some(value),
334 Some("what") => spec.what = Some(value),
335 Some("with") => spec.with = Some(value),
336 Some("details") => spec.details = Some(value),
337 Some("id") => spec.id = Some(value),
338 _ => {
339 return Err(syn::Error::new_spanned(
340 &nv.path,
341 "unknown lifecycle option; expected: stage, when, what, with, details, id",
342 ));
343 }
344 }
345 }
346 }
347
348 Ok(Some(spec))
349 }
350 _ => Ok(None),
351 }
352}
353
354pub fn parse_rust_deprecated(attr: &syn::Attribute) -> Option<LifecycleSpec> {
360 if !attr.path().is_ident("deprecated") {
361 return None;
362 }
363
364 let mut since = None;
365 let mut note = None;
366
367 match &attr.meta {
368 syn::Meta::Path(_) => {
369 }
371 syn::Meta::NameValue(nv) => {
372 if let syn::Expr::Lit(syn::ExprLit {
374 lit: syn::Lit::Str(lit),
375 ..
376 }) = &nv.value
377 {
378 note = Some(lit.value());
379 }
380 }
381 syn::Meta::List(list) => {
382 if let Ok(nested) = list.parse_args_with(
384 syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
385 ) {
386 for meta in nested {
387 if let syn::Meta::NameValue(nv) = meta
388 && let syn::Expr::Lit(syn::ExprLit {
389 lit: syn::Lit::Str(lit),
390 ..
391 }) = &nv.value
392 {
393 if nv.path.is_ident("since") {
394 since = Some(lit.value());
395 } else if nv.path.is_ident("note") {
396 note = Some(lit.value());
397 }
398 }
399 }
400 }
401 }
402 }
403
404 Some(LifecycleSpec::from_deprecated(
405 since.as_deref(),
406 note.as_deref(),
407 ))
408}
409
410pub fn inject_lifecycle_badge(tags: &mut Vec<String>, spec: &LifecycleSpec) {
415 let Some(badge) = spec.stage.badge() else {
416 return;
417 };
418
419 let desc_idx = tags.iter().position(|t| t.starts_with("@description"));
421
422 if let Some(idx) = desc_idx {
423 let existing = &tags[idx];
425 let desc_content = existing
426 .strip_prefix("@description")
427 .unwrap_or("")
428 .trim_start();
429 tags[idx] = format!("@description {} {}", badge, desc_content);
430 } else {
431 tags.insert(0, format!("@description {}", badge));
433 }
434
435 if let Some(keywords) = spec.stage.keywords()
437 && !tags.iter().any(|t| t.starts_with("@keywords"))
438 {
439 tags.push(format!("@keywords {}", keywords));
440 }
441
442 inject_lifecycle_imports(tags, spec);
444}
445
446pub fn inject_lifecycle_imports(tags: &mut Vec<String>, spec: &LifecycleSpec) {
451 let mut fns_to_import = Vec::new();
453
454 if spec.stage.badge().is_some() {
456 fns_to_import.push("badge");
457 }
458
459 if let Some(fn_name) = spec.stage.import_from_fn() {
461 fns_to_import.push(fn_name);
462 }
463
464 if fns_to_import.is_empty() {
465 return;
466 }
467
468 let already_imported: Vec<&str> = tags
470 .iter()
471 .filter_map(|t| t.strip_prefix("@importFrom lifecycle "))
472 .flat_map(|s| s.split_whitespace())
473 .collect();
474
475 fns_to_import.retain(|f| !already_imported.contains(f));
476
477 if !fns_to_import.is_empty() {
478 tags.push(format!("@importFrom lifecycle {}", fns_to_import.join(" ")));
479 }
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485
486 #[test]
487 fn test_stage_from_str() {
488 assert_eq!(
489 LifecycleStage::from_str("experimental"),
490 Some(LifecycleStage::Experimental)
491 );
492 assert_eq!(
493 LifecycleStage::from_str("deprecated"),
494 Some(LifecycleStage::Deprecated)
495 );
496 assert_eq!(
497 LifecycleStage::from_str("soft-deprecated"),
498 Some(LifecycleStage::SoftDeprecated)
499 );
500 assert_eq!(LifecycleStage::from_str("unknown"), None);
501 }
502
503 #[test]
504 fn test_r_prelude_deprecated() {
505 let spec = LifecycleSpec {
506 stage: LifecycleStage::Deprecated,
507 when: Some("0.4.0".into()),
508 what: None,
509 with: Some("new_fn()".into()),
510 details: None,
511 id: None,
512 };
513 let prelude = spec.r_prelude("old_fn").unwrap();
514 assert!(prelude.contains("lifecycle::deprecate_warn"));
515 assert!(prelude.contains("0.4.0"));
516 assert!(prelude.contains("old_fn()"));
517 assert!(prelude.contains("new_fn()"));
518 }
519
520 #[test]
521 fn test_r_prelude_experimental() {
522 let spec = LifecycleSpec::new(LifecycleStage::Experimental);
523 let prelude = spec.r_prelude("my_fn").unwrap();
524 assert!(prelude.contains("lifecycle::signal_stage"));
525 assert!(prelude.contains("experimental"));
526 assert!(prelude.contains("my_fn()"));
527 }
528
529 #[test]
530 fn test_from_deprecated_note() {
531 let spec = LifecycleSpec::from_deprecated(Some("1.0.0"), Some("Use bar() instead"));
532 assert_eq!(spec.stage, LifecycleStage::Deprecated);
533 assert_eq!(spec.when, Some("1.0.0".into()));
534 assert_eq!(spec.with, Some("bar()".into()));
535 }
536
537 #[test]
538 fn test_import_from_fn() {
539 assert_eq!(
540 LifecycleStage::Experimental.import_from_fn(),
541 Some("signal_stage")
542 );
543 assert_eq!(LifecycleStage::Stable.import_from_fn(), None);
544 assert_eq!(
545 LifecycleStage::Superseded.import_from_fn(),
546 Some("signal_stage")
547 );
548 assert_eq!(
549 LifecycleStage::SoftDeprecated.import_from_fn(),
550 Some("deprecate_soft")
551 );
552 assert_eq!(
553 LifecycleStage::Deprecated.import_from_fn(),
554 Some("deprecate_warn")
555 );
556 assert_eq!(
557 LifecycleStage::Defunct.import_from_fn(),
558 Some("deprecate_stop")
559 );
560 }
561
562 #[test]
563 fn test_inject_lifecycle_imports_deprecated() {
564 let spec = LifecycleSpec::new(LifecycleStage::Deprecated);
565 let mut tags = vec!["@title My function".to_string()];
566 inject_lifecycle_badge(&mut tags, &spec);
567 let import_tag = tags
569 .iter()
570 .find(|t| t.starts_with("@importFrom lifecycle"))
571 .expect("should have @importFrom lifecycle tag");
572 assert!(import_tag.contains("badge"));
573 assert!(import_tag.contains("deprecate_warn"));
574 }
575
576 #[test]
577 fn test_inject_lifecycle_imports_experimental() {
578 let spec = LifecycleSpec::new(LifecycleStage::Experimental);
579 let mut tags = vec!["@title My function".to_string()];
580 inject_lifecycle_badge(&mut tags, &spec);
581 let import_tag = tags
582 .iter()
583 .find(|t| t.starts_with("@importFrom lifecycle"))
584 .expect("should have @importFrom lifecycle tag");
585 assert!(import_tag.contains("badge"));
586 assert!(import_tag.contains("signal_stage"));
587 }
588
589 #[test]
590 fn test_inject_lifecycle_imports_stable_no_import() {
591 let spec = LifecycleSpec::new(LifecycleStage::Stable);
592 let mut tags = vec!["@title My function".to_string()];
593 inject_lifecycle_badge(&mut tags, &spec);
594 assert!(!tags.iter().any(|t| t.starts_with("@importFrom")));
596 }
597
598 #[test]
599 fn test_inject_lifecycle_imports_no_duplicates() {
600 let spec = LifecycleSpec::new(LifecycleStage::Deprecated);
601 let mut tags = vec![
602 "@title My function".to_string(),
603 "@importFrom lifecycle deprecate_warn badge".to_string(),
604 ];
605 inject_lifecycle_badge(&mut tags, &spec);
606 let import_count = tags
608 .iter()
609 .filter(|t| t.starts_with("@importFrom lifecycle"))
610 .count();
611 assert_eq!(import_count, 1);
612 }
613
614 #[test]
615 fn test_collect_lifecycle_imports_mixed_methods() {
616 let specs = [
617 LifecycleSpec::new(LifecycleStage::Deprecated),
618 LifecycleSpec::new(LifecycleStage::Experimental),
619 LifecycleSpec::new(LifecycleStage::Stable),
620 ];
621 let result = collect_lifecycle_imports(specs.iter());
622 let import = result.expect("should produce import tag");
623 assert_eq!(
625 import,
626 "@importFrom lifecycle badge deprecate_warn signal_stage"
627 );
628 }
629
630 #[test]
631 fn test_collect_lifecycle_imports_no_lifecycle() {
632 let specs: Vec<LifecycleSpec> = vec![];
633 let result = collect_lifecycle_imports(specs.iter());
634 assert!(result.is_none());
635 }
636
637 #[test]
638 fn test_collect_lifecycle_imports_only_stable() {
639 let specs = [LifecycleSpec::new(LifecycleStage::Stable)];
640 let result = collect_lifecycle_imports(specs.iter());
641 assert!(result.is_none());
642 }
643}