1use std::collections::{HashMap, HashSet};
2
3use regex::Regex;
4use serde_json::Value;
5
6use super::types::{Item, RustdocJson};
7
8pub fn type_to_string(ty: &Value) -> String {
14 if ty.is_null() {
15 return "()".to_string();
16 }
17
18 let obj = match ty.as_object() {
19 Some(o) => o,
20 None => return ty.to_string(),
21 };
22
23 if let Some(p) = obj.get("primitive").and_then(|v| v.as_str()) {
25 return p.to_string();
26 }
27
28 if let Some(g) = obj.get("generic").and_then(|v| v.as_str()) {
30 return g.to_string();
31 }
32
33 if let Some(rp) = obj.get("resolved_path") {
35 let name = rp.get("path")
36 .or_else(|| rp.get("name"))
37 .and_then(|v| v.as_str())
38 .unwrap_or("_");
39 let args = rp.get("args")
40 .and_then(|a| a.get("angle_bracketed"))
41 .and_then(|ab| ab.get("args"))
42 .and_then(|a| a.as_array());
43 if let Some(args) = args {
44 let type_args: Vec<String> = args.iter()
45 .filter_map(|a| a.get("type").map(type_to_string))
46 .collect();
47 if !type_args.is_empty() {
48 return format!("{name}<{}>", type_args.join(", "));
49 }
50 }
51 return name.to_string();
52 }
53
54 if let Some(br) = obj.get("borrowed_ref") {
56 let lifetime = br.get("lifetime").and_then(|v| v.as_str());
57 let mutable = br.get("mutable").and_then(|v| v.as_bool()).unwrap_or(false);
58 let inner = br.get("type").map(type_to_string).unwrap_or_else(|| "_".to_string());
59 let mut_str = if mutable { "mut " } else { "" };
60 return match lifetime {
61 Some(lt) if !lt.is_empty() => {
62 if lt.starts_with('\'') {
65 format!("&{lt} {mut_str}{inner}")
66 } else {
67 format!("&'{lt} {mut_str}{inner}")
68 }
69 },
70 _ => format!("&{mut_str}{inner}"),
71 };
72 }
73
74 if let Some(tup) = obj.get("tuple").and_then(|v| v.as_array()) {
76 let parts: Vec<String> = tup.iter().map(type_to_string).collect();
77 return format!("({})", parts.join(", "));
78 }
79
80 if let Some(sl) = obj.get("slice") {
82 return format!("[{}]", type_to_string(sl));
83 }
84
85 if let Some(arr) = obj.get("array") {
87 let elem = arr.get("type").map(type_to_string).unwrap_or_else(|| "_".to_string());
88 let len = arr.get("len").and_then(|v| v.as_str()).unwrap_or("_");
89 return format!("[{elem}; {len}]");
90 }
91
92 if let Some(rp) = obj.get("raw_pointer") {
94 let mutable = rp.get("mutable").and_then(|v| v.as_bool()).unwrap_or(false);
95 let inner = rp.get("type").map(type_to_string).unwrap_or_else(|| "_".to_string());
96 let mut_str = if mutable { "mut" } else { "const" };
97 return format!("*{mut_str} {inner}");
98 }
99
100 if let Some(bounds) = obj.get("impl_trait").and_then(|v| v.as_array()) {
102 let parts: Vec<String> = bounds.iter()
103 .filter_map(|b| b.get("trait_bound"))
104 .filter_map(|tb| tb.get("trait"))
105 .map(type_to_string)
106 .collect();
107 return format!("impl {}", parts.join(" + "));
108 }
109
110 if let Some(dt) = obj.get("dyn_trait") {
112 let traits = dt.get("traits")
113 .and_then(|v| v.as_array())
114 .map(|ts| {
115 ts.iter()
116 .filter_map(|t| t.get("trait"))
117 .map(type_to_string)
118 .collect::<Vec<_>>()
119 .join(" + ")
120 })
121 .unwrap_or_default();
122 let lifetime = dt.get("lifetime").and_then(|v| v.as_str());
123 return match lifetime {
124 Some(lt) if !lt.is_empty() => format!("dyn {traits} + {lt}"),
125 _ => format!("dyn {traits}"),
126 };
127 }
128
129 if let Some(fp) = obj.get("function_pointer") {
131 let decl = fp.get("sig")
132 .or_else(|| fp.get("decl"));
133 if let Some(decl) = decl {
134 let inputs = decl.get("inputs")
135 .and_then(|v| v.as_array())
136 .map(|inputs| {
137 inputs.iter()
138 .filter_map(|i| i.as_array())
139 .map(|pair| {
140 let name = pair.first().and_then(|v| v.as_str()).unwrap_or("_");
141 let ty = pair.get(1).map(type_to_string).unwrap_or_else(|| "_".to_string());
142 format!("{name}: {ty}")
143 })
144 .collect::<Vec<_>>()
145 .join(", ")
146 })
147 .unwrap_or_default();
148 let output = decl.get("output").map(type_to_string).unwrap_or_default();
149 if output.is_empty() || output == "()" {
150 return format!("fn({inputs})");
151 } else {
152 return format!("fn({inputs}) -> {output}");
153 }
154 }
155 }
156
157 if let Some(qp) = obj.get("qualified_path") {
159 let self_type = qp.get("self_type").map(type_to_string).unwrap_or_else(|| "_".to_string());
160 let name = qp.get("name").and_then(|v| v.as_str()).unwrap_or("_");
161 let trait_val = qp.get("trait");
162 let trait_is_absent = trait_val.map(|v| v.is_null()).unwrap_or(true);
163 if trait_is_absent {
164 return format!("{self_type}::{name}");
166 }
167 let trait_name = trait_val.map(type_to_string).unwrap_or_default();
168 return format!("<{self_type} as {trait_name}>::{name}");
169 }
170
171 if obj.contains_key("id") {
174 if let Some(path_str) = obj.get("path").and_then(|v| v.as_str()) {
175 let name = if path_str.is_empty() { "_" } else { path_str };
176 let args = obj.get("args")
177 .and_then(|a| a.get("angle_bracketed"))
178 .and_then(|ab| ab.get("args"))
179 .and_then(|a| a.as_array());
180 if let Some(args) = args {
181 let type_args: Vec<String> = args.iter()
182 .filter_map(|a| a.get("type").map(type_to_string))
183 .collect();
184 if !type_args.is_empty() {
185 return format!("{name}<{}>", type_args.join(", "));
186 }
187 }
188 return name.to_string();
189 }
190 }
191
192 ty.to_string()
194}
195
196pub fn function_signature(item: &Item) -> String {
200 let inner = match item.inner_for("function") {
201 Some(f) => f,
202 None => return String::new(),
203 };
204
205 let header = inner.get("header");
206 let is_async = header.and_then(|h| h.get("is_async")).and_then(|v| v.as_bool()).unwrap_or(false);
207 let is_const = header.and_then(|h| h.get("is_const")).and_then(|v| v.as_bool()).unwrap_or(false);
208 let is_unsafe = header.and_then(|h| h.get("is_unsafe")).and_then(|v| v.as_bool()).unwrap_or(false);
209
210 let sig = match inner.get("sig") {
211 Some(s) => s,
212 None => return String::new(),
213 };
214
215 let name = item.name.as_deref().unwrap_or("_");
216
217 let generics = inner.get("generics");
219 let generic_str = format_generics(generics);
220
221 let inputs = sig.get("inputs")
223 .and_then(|v| v.as_array())
224 .map(|inputs| {
225 inputs.iter()
226 .filter_map(|i| i.as_array())
227 .map(|pair| {
228 let param_name = pair.first().and_then(|v| v.as_str()).unwrap_or("_");
229 let ty = pair.get(1).map(type_to_string).unwrap_or_else(|| "_".to_string());
230 if param_name == "self" {
232 match ty.as_str() {
233 "Self" => "self".to_string(),
234 "&Self" => "&self".to_string(),
235 "&mut Self" => "&mut self".to_string(),
236 _ => format!("self: {ty}"),
237 }
238 } else {
239 format!("{param_name}: {ty}")
240 }
241 })
242 .collect::<Vec<_>>()
243 .join(", ")
244 })
245 .unwrap_or_default();
246
247 let output = sig.get("output")
248 .filter(|v| !v.is_null())
249 .map(type_to_string);
250
251 let where_str = format_where(generics);
252
253 let mut prefix = String::new();
254 if is_const { prefix.push_str("const "); }
255 if is_async { prefix.push_str("async "); }
256 if is_unsafe { prefix.push_str("unsafe "); }
257
258 let output_str = match &output {
259 Some(s) if s != "()" => format!(" -> {s}"),
260 _ => String::new(),
261 };
262
263 format!("{prefix}fn {name}{generic_str}({inputs}){output_str}{where_str}")
264}
265
266pub fn struct_fields(item: &Item) -> Vec<String> {
268 let inner = match item.inner_for("struct") {
269 Some(s) => s,
270 None => return vec![],
271 };
272
273 let kind = inner.get("kind");
274 if let Some(plain) = kind.and_then(|k| k.get("plain")) {
275 let fields = plain.get("fields")
276 .and_then(|f| f.as_array())
277 .map(|v| v.as_slice()).unwrap_or(&[]);
278 fields.iter()
279 .filter_map(|id| id.as_str())
280 .map(|_id| "/* field */".to_string()) .collect()
282 } else {
283 vec![]
284 }
285}
286
287pub fn format_generics_for_item(item: &Item, kind: &str) -> String {
290 for k in &[kind, "struct", "enum", "union", "trait", "type_alias", "typedef"] {
291 if let Some(inner) = item.inner_for(k) {
292 if let Some(generics) = inner.get("generics") {
293 let s = format_generics(Some(generics));
294 if !s.is_empty() {
295 return s;
296 }
297 }
298 }
299 }
300 String::new()
301}
302
303fn format_generics(generics: Option<&Value>) -> String {
304 let generics = match generics {
305 Some(g) => g,
306 None => return String::new(),
307 };
308 let params = match generics.get("params").and_then(|v| v.as_array()) {
309 Some(p) => p,
310 None => return String::new(),
311 };
312 if params.is_empty() {
313 return String::new();
314 }
315 let parts: Vec<String> = params.iter()
316 .filter_map(|p| {
317 let name = p.get("name")?.as_str()?;
318 if name.starts_with("impl ") {
321 return None;
322 }
323 let kind = p.get("kind");
324 if let Some(const_info) = kind.and_then(|k| k.get("const")) {
326 let ty_str = const_info.get("type").map(type_to_string).unwrap_or_else(|| "_".to_string());
327 return Some(format!("const {name}: {ty_str}"));
328 }
329 if let Some(type_bounds) = kind.and_then(|k| k.get("type")).and_then(|t| t.get("bounds")) {
331 let bounds = type_bounds.as_array()
332 .map(|bs| {
333 bs.iter()
334 .filter_map(|b| b.get("trait_bound"))
335 .filter_map(|tb| tb.get("trait"))
336 .map(type_to_string)
337 .collect::<Vec<_>>()
338 .join(" + ")
339 })
340 .unwrap_or_default();
341 if bounds.is_empty() {
342 Some(name.to_string())
343 } else {
344 Some(format!("{name}: {bounds}"))
345 }
346 } else {
347 Some(name.to_string())
349 }
350 })
351 .collect();
352 if parts.is_empty() {
353 String::new()
354 } else {
355 format!("<{}>", parts.join(", "))
356 }
357}
358
359fn format_where(generics: Option<&Value>) -> String {
360 let generics = match generics {
361 Some(g) => g,
362 None => return String::new(),
363 };
364 let clauses = match generics.get("where_predicates").and_then(|v| v.as_array()) {
365 Some(c) => c,
366 None => return String::new(),
367 };
368 if clauses.is_empty() {
369 return String::new();
370 }
371 let parts: Vec<String> = clauses.iter()
372 .filter_map(|c| {
373 if let Some(bp) = c.get("bound_predicate") {
374 let ty = bp.get("type").map(type_to_string)?;
375 let bounds = bp.get("bounds")?.as_array()?;
376 let bound_strs: Vec<String> = bounds.iter()
377 .filter_map(|b| b.get("trait_bound"))
378 .filter_map(|tb| tb.get("trait"))
379 .map(type_to_string)
380 .collect();
381 if bound_strs.is_empty() {
382 None
383 } else {
384 Some(format!("{ty}: {}", bound_strs.join(" + ")))
385 }
386 } else {
387 None
388 }
389 })
390 .collect();
391 if parts.is_empty() {
392 String::new()
393 } else {
394 format!("\nwhere\n {}", parts.join(",\n "))
395 }
396}
397
398pub fn extract_feature_requirements(
407 attrs: &[String],
408 declared_features: &HashSet<String>,
409) -> Vec<String> {
410 let Ok(re) = Regex::new(r#"name: "feature", value: Some\("([^"]+)"\)"#) else {
413 return vec![];
414 };
415
416 let mut features: Vec<String> = attrs
417 .iter()
418 .flat_map(|attr| {
419 re.captures_iter(attr)
420 .filter_map(|cap| cap.get(1).map(|m| m.as_str().to_string()))
421 .collect::<Vec<_>>()
422 })
423 .collect();
424
425 if !declared_features.is_empty() {
427 features.retain(|f| declared_features.contains(f));
428 }
429
430 features.sort();
431 features.dedup();
432 features
433}
434
435#[derive(Debug, Clone)]
439pub struct ItemSummary {
440 pub kind: String,
441 pub name: String,
442 pub doc_summary: String,
443}
444
445#[derive(Debug, Clone)]
446pub struct ModuleNode {
447 pub path: String,
448 pub doc_summary: String,
449 pub item_counts: HashMap<String, usize>,
451 pub items: Vec<ItemSummary>,
453 pub children: Vec<ModuleNode>,
454}
455
456pub fn build_module_tree(doc: &RustdocJson) -> Vec<ModuleNode> {
457 let root_id = doc.root_id();
459 let root_item = doc.index.get(&root_id);
460 if root_item.is_none() {
461 return vec![];
462 }
463
464 if let Some(root) = root_item {
466 if let Some(module) = root.inner_for("module") {
467 let item_ids = module.get("items")
468 .and_then(|v| v.as_array())
469 .cloned()
470 .unwrap_or_default();
471
472 return build_children(&item_ids, doc, 0);
473 }
474 }
475 vec![]
476}
477
478fn id_val_to_string(id_val: &Value) -> Option<String> {
479 match id_val {
480 Value::String(s) => Some(s.clone()),
481 Value::Number(n) => Some(n.to_string()),
482 _ => None,
483 }
484}
485
486fn build_children(item_ids: &[Value], doc: &RustdocJson, depth: usize) -> Vec<ModuleNode> {
487 if depth > 5 {
488 return vec![];
489 }
490
491 let mut modules = vec![];
492 let mut other_counts: HashMap<String, usize> = HashMap::new();
493
494 for id_val in item_ids {
495 let id = match id_val_to_string(id_val) {
497 Some(s) => s,
498 None => continue,
499 };
500
501 let item = match doc.index.get(&id) {
502 Some(i) => i,
503 None => continue,
504 };
505
506 let kind = item.kind().unwrap_or("unknown");
507
508 if kind == "module" {
509 let path = doc.paths.get(&id)
510 .map(|p| p.full_path())
511 .or_else(|| item.name.clone())
512 .unwrap_or_else(|| id.clone());
513
514 let doc_summary = item.doc_summary();
515
516 let sub_items = item.inner_for("module")
517 .and_then(|m| m.get("items"))
518 .and_then(|v| v.as_array())
519 .cloned()
520 .unwrap_or_default();
521
522 let mut item_counts = HashMap::new();
523 let mut direct_items = vec![];
524 for sub_id_val in &sub_items {
525 if let Some(sub_id) = id_val_to_string(sub_id_val) {
526 if let Some(sub_item) = doc.index.get(&sub_id) {
527 if let Some(k) = sub_item.kind() {
528 if k == "use" || k == "import" { continue; }
531 *item_counts.entry(k.to_string()).or_insert(0) += 1;
532 if k != "module" {
534 direct_items.push(ItemSummary {
535 kind: k.to_string(),
536 name: sub_item.name.clone().unwrap_or_default(),
537 doc_summary: sub_item.doc_summary(),
538 });
539 }
540 }
541 }
542 }
543 }
544
545 let children = build_children(&sub_items, doc, depth + 1);
546
547 modules.push(ModuleNode {
548 path,
549 doc_summary,
550 item_counts,
551 items: direct_items,
552 children,
553 });
554 } else {
555 *other_counts.entry(kind.to_string()).or_insert(0) += 1;
556 }
557 }
558
559 modules
560}
561
562fn type_item_id(val: &Value) -> Option<String> {
566 if let Some(rp) = val.get("resolved_path") {
567 return match rp.get("id") {
568 Some(Value::Number(n)) => Some(n.to_string()),
569 Some(Value::String(s)) => Some(s.clone()),
570 _ => None,
571 };
572 }
573 match (val.get("id"), val.get("path")) {
574 (Some(Value::Number(n)), Some(_)) => Some(n.to_string()),
575 (Some(Value::String(s)), Some(_)) => Some(s.clone()),
576 _ => None,
577 }
578}
579
580fn build_method_parent_map(doc: &RustdocJson) -> HashMap<String, String> {
585 let mut map: HashMap<String, String> = HashMap::new();
586
587 for item in doc.index.values() {
588 if item.kind() != Some("impl") { continue; }
589 let Some(impl_inner) = item.inner_for("impl") else { continue };
590
591 let trait_is_null = impl_inner.get("trait").map(|t| t.is_null()).unwrap_or(true);
593 if !trait_is_null { continue; }
594
595 let Some(for_val) = impl_inner.get("for") else { continue };
596
597 let parent_path = type_item_id(for_val)
600 .and_then(|id| doc.paths.get(&id))
601 .map(|p| p.full_path())
602 .unwrap_or_else(|| type_to_string(for_val));
603
604 if parent_path.is_empty() { continue; }
605
606 let method_ids = impl_inner.get("items")
607 .and_then(|v| v.as_array())
608 .cloned()
609 .unwrap_or_default();
610
611 for method_id_val in &method_ids {
612 if let Some(mid) = id_val_to_string(method_id_val) {
613 map.insert(mid, parent_path.clone());
614 }
615 }
616 }
617
618 map
619}
620
621pub struct SearchResult {
624 pub path: String,
625 pub kind: String,
626 pub signature: String,
627 pub doc_summary: String,
628 pub feature_requirements: Vec<String>,
629 pub score: f32,
630}
631
632pub fn search_items(
634 doc: &RustdocJson,
635 query: &str,
636 kind_filter: Option<&str>,
637 module_prefix: Option<&str>,
638 limit: usize,
639 declared_features: &HashSet<String>,
640) -> Vec<SearchResult> {
641 let query_lower = query.to_lowercase();
642 let mut results: Vec<SearchResult> = vec![];
643
644 for (id, item) in &doc.index {
645 let path_entry = match doc.paths.get(id) {
646 Some(p) => p,
647 None => continue,
648 };
649
650 let full_path = path_entry.full_path();
651 let name = item.name.as_deref().unwrap_or("");
652 let item_kind = path_entry.kind_name();
653
654 if let Some(kf) = kind_filter {
656 let normalized = match kf {
657 "fn" => "function",
658 "mod" => "module",
659 "type" => "type_alias",
660 other => other,
661 };
662 if item_kind != normalized {
663 continue;
664 }
665 }
666
667 if let Some(prefix) = module_prefix {
669 if !full_path.starts_with(prefix) {
670 continue;
671 }
672 }
673
674 if name.is_empty() {
676 continue;
677 }
678
679 let name_lower = name.to_lowercase();
680 let doc_summary = item.doc_summary();
681 let doc_lower = doc_summary.to_lowercase();
682
683 let score = if name_lower == query_lower {
685 1.0f32
686 } else if name_lower.starts_with(&query_lower) {
687 0.9
688 } else if name_lower.contains(&query_lower) {
689 0.7
690 } else if doc_lower.contains(&query_lower) {
691 0.2
692 } else {
693 continue; };
695
696 let signature = match item.kind().unwrap_or("") {
697 "function" => function_signature(item),
698 _ => format!("{} {}", item_kind, name),
699 };
700
701 let feature_requirements = extract_feature_requirements(&item.attr_strings(), declared_features);
702
703 results.push(SearchResult {
704 path: full_path,
705 kind: item_kind.to_string(),
706 signature,
707 doc_summary,
708 feature_requirements,
709 score,
710 });
711 }
712
713 let want_methods = kind_filter.is_none() || kind_filter == Some("method");
717
718 if want_methods {
719 let method_parent_map = build_method_parent_map(doc);
720
721 for (id, item) in &doc.index {
722 if doc.paths.contains_key(id) { continue; } if item.kind() != Some("function") { continue; }
724
725 let Some(parent_path) = method_parent_map.get(id) else { continue };
726 let name = item.name.as_deref().unwrap_or("");
727 if name.is_empty() { continue; }
728
729 if let Some(prefix) = module_prefix {
731 if !parent_path.starts_with(prefix) { continue; }
732 }
733
734 let name_lower = name.to_lowercase();
735 let parent_lower = parent_path.to_lowercase();
736 let doc_summary = item.doc_summary();
737 let doc_lower = doc_summary.to_lowercase();
738
739 let score = if name_lower == query_lower {
740 1.0f32
741 } else if name_lower.starts_with(&query_lower) {
742 0.9
743 } else if name_lower.contains(&query_lower) {
744 0.7
745 } else if parent_lower.contains(&query_lower) {
746 0.6 } else if doc_lower.contains(&query_lower) {
748 0.4
749 } else {
750 continue;
751 };
752
753 let full_path = format!("{parent_path}::{name}");
754 let signature = function_signature(item);
755 let feature_requirements = extract_feature_requirements(&item.attr_strings(), declared_features);
756
757 results.push(SearchResult {
758 path: full_path,
759 kind: "method".to_string(),
760 signature,
761 doc_summary,
762 feature_requirements,
763 score,
764 });
765 }
766 }
767
768 results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
770 results.truncate(limit);
771 results
772}
773
774#[cfg(test)]
775mod tests {
776 use super::*;
777
778 #[test]
779 fn test_type_to_string_primitive() {
780 let ty = serde_json::json!({"primitive": "str"});
781 assert_eq!(type_to_string(&ty), "str");
782 }
783
784 #[test]
785 fn test_type_to_string_generic() {
786 let ty = serde_json::json!({"generic": "T"});
787 assert_eq!(type_to_string(&ty), "T");
788 }
789
790 #[test]
791 fn test_type_to_string_ref() {
792 let ty = serde_json::json!({
793 "borrowed_ref": {
794 "lifetime": null,
795 "mutable": false,
796 "type": {"primitive": "str"}
797 }
798 });
799 assert_eq!(type_to_string(&ty), "&str");
800 }
801
802 #[test]
803 fn test_type_to_string_mut_ref_with_lifetime() {
804 let ty = serde_json::json!({
805 "borrowed_ref": {
806 "lifetime": "a",
807 "mutable": true,
808 "type": {"generic": "T"}
809 }
810 });
811 assert_eq!(type_to_string(&ty), "&'a mut T");
812 }
813
814 #[test]
815 fn test_type_to_string_tuple() {
816 let ty = serde_json::json!({
817 "tuple": [
818 {"primitive": "i32"},
819 {"primitive": "bool"}
820 ]
821 });
822 assert_eq!(type_to_string(&ty), "(i32, bool)");
823 }
824
825 #[test]
826 fn test_type_to_string_slice() {
827 let ty = serde_json::json!({"slice": {"primitive": "u8"}});
828 assert_eq!(type_to_string(&ty), "[u8]");
829 }
830
831 #[test]
832 fn test_type_to_string_option() {
833 let ty = serde_json::json!({
834 "resolved_path": {
835 "path": "Option",
836 "args": {
837 "angle_bracketed": {
838 "args": [
839 {"type": {"primitive": "i32"}}
840 ]
841 }
842 }
843 }
844 });
845 assert_eq!(type_to_string(&ty), "Option<i32>");
846 }
847
848 #[test]
849 fn test_feature_regex_correct_pattern() {
850 let attr = r#"#[attr = CfgTrace([NameValue { name: "feature", value: Some("auth"), span: None }])]"#;
851 let features = extract_feature_requirements(
852 &[attr.to_string()],
853 &HashSet::from(["auth".to_string()]),
854 );
855 assert_eq!(features, vec!["auth"]);
856 }
857
858 #[test]
859 fn test_feature_regex_old_pattern_fails() {
860 let attr = r#"#[attr = CfgTrace([NameValue { name: "feature", value: Some("auth"), span: None }])]"#;
862 let old_re = regex::Regex::new(r#"#\[cfg\(feature\s*=\s*"([^"]+)"\)\]"#).unwrap();
864 let matches: Vec<&str> = old_re.captures_iter(attr)
865 .filter_map(|c| c.get(1).map(|m| m.as_str()))
866 .collect();
867 assert!(matches.is_empty(), "Old pattern should NOT match v57 attr format");
868 }
869
870 #[test]
871 fn test_feature_cross_reference() {
872 let attr = r#"#[attr = CfgTrace([NameValue { name: "feature", value: Some("undeclared"), span: None }])]"#;
873 let declared = HashSet::from(["auth".to_string(), "tls".to_string()]);
874 let features = extract_feature_requirements(&[attr.to_string()], &declared);
875 assert!(features.is_empty());
877 }
878}