1use std::collections::{BTreeMap, HashMap};
10use std::path::Path;
11
12use serde_json::Value;
13
14#[derive(Debug, Clone)]
18pub struct ItemInfo {
19 pub kind: String,
21 pub fingerprint: u64,
23 pub signature: String,
27 pub has_docs: bool,
29}
30
31#[derive(Debug, Default)]
33pub struct Surface {
34 pub items: BTreeMap<String, ItemInfo>,
35 pub members: BTreeMap<String, ItemInfo>,
39}
40
41pub struct ItemGroup<'a> {
47 pub representative: &'a String,
48 pub aliases: Vec<&'a String>,
49}
50
51impl Surface {
52 pub fn lookup(&self, path: &str) -> Option<&ItemInfo> {
54 self.items.get(path).or_else(|| self.members.get(path))
55 }
56
57 pub fn grouped(&self) -> Vec<ItemGroup<'_>> {
59 let mut groups: HashMap<(&str, u64), Vec<&String>> = HashMap::new();
60 let mut unfingerprinted = Vec::new();
61 for (path, info) in &self.items {
62 if info.fingerprint == 0 {
63 unfingerprinted.push(path);
64 continue;
65 }
66 groups
67 .entry((info.kind.as_str(), info.fingerprint))
68 .or_default()
69 .push(path);
70 }
71 let mut out: Vec<ItemGroup<'_>> = groups
72 .into_values()
73 .map(|mut aliases| {
74 aliases.sort();
75 let representative = aliases.iter().min_by_key(|p| p.len()).copied().unwrap();
76 ItemGroup {
77 representative,
78 aliases,
79 }
80 })
81 .chain(unfingerprinted.into_iter().map(|p| ItemGroup {
82 representative: p,
83 aliases: vec![p],
84 }))
85 .collect();
86 out.sort_by(|a, b| a.representative.cmp(b.representative));
87 out
88 }
89}
90
91const KINDS: &[&str] = &[
93 "function",
94 "struct",
95 "enum",
96 "trait",
97 "constant",
98 "type_alias",
99 "module",
100 "static",
101 "union",
102 "macro",
103 "proc_macro",
104];
105
106pub fn from_rustdoc(doc: &Value, source_root: &Path) -> Surface {
109 let mut surface = Surface::default();
110 let (Some(index), Some(paths)) = (doc["index"].as_object(), doc["paths"].as_object()) else {
111 return surface;
112 };
113 let mut file_cache: HashMap<String, Vec<String>> = HashMap::new();
114
115 for (id, item) in index {
116 if item["visibility"] != "public" {
117 continue;
118 }
119 let Some(inner) = item["inner"].as_object() else {
120 continue;
121 };
122 let Some(kind) = inner.keys().next().map(String::as_str) else {
123 continue;
124 };
125 if !KINDS.contains(&kind) {
126 continue;
127 }
128 let Some(full) = item_path(paths, id) else {
130 continue;
131 };
132
133 let (fingerprint, signature) =
134 span_fingerprint(&item["span"], kind, source_root, &mut file_cache)
135 .unwrap_or((0, String::new()));
136 let has_docs = item["docs"].as_str().is_some_and(|d| !d.trim().is_empty());
137
138 surface.items.insert(
139 full,
140 ItemInfo {
141 kind: kind.to_string(),
142 fingerprint,
143 signature,
144 has_docs,
145 },
146 );
147 }
148
149 alias_reexports(index, paths, &mut surface);
150 index_members(index, paths, source_root, &mut file_cache, &mut surface);
151 surface
152}
153
154fn index_members(
157 index: &serde_json::Map<String, Value>,
158 paths: &serde_json::Map<String, Value>,
159 source_root: &Path,
160 file_cache: &mut HashMap<String, Vec<String>>,
161 surface: &mut Surface,
162) {
163 for item in index.values() {
164 let imp = &item["inner"]["impl"];
165 if !imp.is_object() || imp["trait"].is_object() {
166 continue;
167 }
168 let Some(type_id) = imp["for"]["resolved_path"]["id"].as_u64() else {
169 continue;
170 };
171 let Some(type_path) = item_path(paths, &type_id.to_string()) else {
172 continue;
173 };
174 let Some(owner) = surface.items.get(&type_path) else {
175 continue;
176 };
177 let spellings: Vec<String> = surface
178 .items
179 .iter()
180 .filter(|(_, info)| info.kind == owner.kind && info.fingerprint == owner.fingerprint)
181 .map(|(path, _)| path.clone())
182 .collect();
183
184 let members = imp["items"].as_array().into_iter().flatten();
185 for member in members.filter_map(|id| index.get(&id.as_u64()?.to_string())) {
186 if member["visibility"] != "public" {
187 continue;
188 }
189 let (kind, name) = match (
190 member["inner"]["function"].is_object(),
191 member["inner"]["assoc_const"].is_object(),
192 member["name"].as_str(),
193 ) {
194 (true, _, Some(name)) => ("function", name),
195 (_, true, Some(name)) => ("constant", name),
196 _ => continue,
197 };
198 let (fingerprint, signature) =
199 span_fingerprint(&member["span"], kind, source_root, file_cache)
200 .unwrap_or((0, String::new()));
201 let info = ItemInfo {
202 kind: kind.to_string(),
203 fingerprint,
204 signature,
205 has_docs: member["docs"]
206 .as_str()
207 .is_some_and(|d| !d.trim().is_empty()),
208 };
209 for spelling in &spellings {
210 surface
211 .members
212 .insert(format!("{spelling}::{name}"), info.clone());
213 }
214 }
215 }
216}
217
218fn alias_reexports(
230 index: &serde_json::Map<String, Value>,
231 paths: &serde_json::Map<String, Value>,
232 surface: &mut Surface,
233) {
234 for (mod_id, item) in index {
235 if item["visibility"] != "public" {
236 continue;
237 }
238 let Some(children) = item["inner"]["module"]["items"].as_array() else {
239 continue;
240 };
241 let Some(mod_path) = item_path(paths, mod_id) else {
242 continue;
243 };
244
245 for child in children {
246 let Some(child_id) = child.as_u64() else {
247 continue;
248 };
249 let child_id = child_id.to_string();
250 let Some(use_) = index.get(&child_id).map(|c| &c["inner"]["use"]) else {
251 continue;
252 };
253 let Some(target_id) = use_["id"].as_u64() else {
254 continue;
255 };
256 let targets: Vec<(String, u64)> = if use_["is_glob"].as_bool().unwrap_or(false) {
262 glob_targets(index, target_id)
263 } else {
264 match use_["name"].as_str() {
265 Some(name) => vec![(name.to_string(), target_id)],
266 None => continue,
267 }
268 };
269
270 for (name, target_id) in targets {
271 alias_one(paths, surface, &mod_path, &name, target_id);
272 }
273 }
274 }
275}
276
277fn glob_targets(index: &serde_json::Map<String, Value>, module_id: u64) -> Vec<(String, u64)> {
280 let Some(children) = index
281 .get(&module_id.to_string())
282 .and_then(|m| m["inner"]["module"]["items"].as_array())
283 else {
284 return Vec::new();
285 };
286 children
287 .iter()
288 .filter_map(|c| {
289 let id = c.as_u64()?;
290 let name = index.get(&id.to_string())?["name"].as_str()?;
291 Some((name.to_string(), id))
292 })
293 .collect()
294}
295
296fn alias_one(
301 paths: &serde_json::Map<String, Value>,
302 surface: &mut Surface,
303 mod_path: &str,
304 name: &str,
305 target_id: u64,
306) {
307 let Some(target_path) = item_path(paths, &target_id.to_string()) else {
308 return;
309 };
310 let Some(info) = surface.items.get(&target_path).cloned() else {
311 return; };
313 let alias = format!("{mod_path}::{name}");
314 if alias == target_path {
315 return;
316 }
317 surface.items.entry(alias).or_insert(info);
318}
319
320fn item_path(paths: &serde_json::Map<String, Value>, id: &str) -> Option<String> {
321 let segs = paths.get(id)?["path"].as_array()?;
322 Some(
323 segs.iter()
324 .filter_map(Value::as_str)
325 .collect::<Vec<_>>()
326 .join("::"),
327 )
328}
329
330const MEMBER_KINDS: [&str; 3] = ["struct", "enum", "union"];
334
335fn span_fingerprint(
336 span: &Value,
337 kind: &str,
338 root: &Path,
339 cache: &mut HashMap<String, Vec<String>>,
340) -> Option<(u64, String)> {
341 let filename = span["filename"].as_str()?;
342 let begin = span["begin"].as_array()?;
343 let end = span["end"].as_array()?;
344 let (bl, el) = (begin[0].as_u64()? as usize, end[0].as_u64()? as usize);
345
346 let lines = match cache.get(filename) {
347 Some(l) => l,
348 None => {
349 let abs = root.join(filename);
352 let text = std::fs::read_to_string(&abs).ok()?;
353 cache.insert(
354 filename.to_string(),
355 text.lines().map(str::to_string).collect(),
356 );
357 &cache[filename]
358 }
359 };
360 if bl == 0 || el > lines.len() || bl > el {
361 return None;
362 }
363 let span_text = lines[bl - 1..el].join("\n");
364 let normalized = normalize(&crate::comments::strip(&span_text));
365 let bare = normalize(&crate::comments::strip_all(&span_text));
368 let sig_end = bare
369 .find('{')
370 .or_else(|| bare.find(';'))
371 .unwrap_or(bare.len());
372 let declaration = bare[..sig_end].trim();
373 let signature = if MEMBER_KINDS.contains(&kind) {
374 member_signature(declaration, &bare[sig_end..], kind == "enum")
375 } else {
376 declaration.to_string()
377 };
378 Some((soothfast_registry::fnv1a(normalized.as_bytes()), signature))
379}
380
381fn normalize(source: &str) -> String {
382 source.split_whitespace().collect::<Vec<_>>().join(" ")
383}
384
385fn member_signature(declaration: &str, rest: &str, all_public: bool) -> String {
389 let Some(body) = rest
390 .strip_prefix('{')
391 .and_then(|inner| inner.trim_end().strip_suffix('}'))
392 else {
393 return declaration.to_string();
394 };
395 let members: Vec<String> = split_top_level(body)
396 .into_iter()
397 .map(|member| strip_attributes(member.trim()).to_string())
398 .filter(|member| !member.is_empty() && (all_public || member.starts_with("pub ")))
399 .collect();
400 format!("{declaration} {{ {} }}", members.join(", "))
401}
402
403fn split_top_level(text: &str) -> Vec<&str> {
406 let mut parts = Vec::new();
407 let mut depth = 0i32;
408 let mut start = 0;
409 let mut prev = ' ';
410 for (i, c) in text.char_indices() {
411 match c {
412 '<' | '(' | '[' | '{' => depth += 1,
413 '>' if prev == '-' => {}
414 '>' | ')' | ']' | '}' => depth -= 1,
415 ',' if depth == 0 => {
416 parts.push(&text[start..i]);
417 start = i + 1;
418 }
419 _ => {}
420 }
421 prev = c;
422 }
423 parts.push(&text[start..]);
424 parts
425}
426
427fn strip_attributes(member: &str) -> &str {
430 let mut rest = member;
431 while let Some(after) = rest.strip_prefix("#[") {
432 let mut depth = 1;
433 let mut end = None;
434 for (i, c) in after.char_indices() {
435 depth += match c {
436 '[' => 1,
437 ']' => -1,
438 _ => 0,
439 };
440 if depth == 0 {
441 end = Some(i + 1);
442 break;
443 }
444 }
445 match end {
446 Some(e) => rest = after[e..].trim_start(),
447 None => break,
448 }
449 }
450 rest
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456 use serde_json::json;
457
458 fn private_submodule_reexport_doc() -> Value {
461 json!({
462 "index": {
463 "1": {
464 "visibility": "public",
465 "inner": { "module": { "items": [2, 3] } },
466 },
467 "2": {
468 "name": "sma",
469 "visibility": "public",
470 "inner": { "function": {} },
471 "span": { "filename": "src/sma.rs", "begin": [1, 0], "end": [1, 0] },
472 "docs": "Simple moving average.",
473 },
474 "3": {
475 "visibility": "public",
476 "inner": {
477 "use": { "source": "sma::sma", "name": "sma", "id": 2, "is_glob": false },
478 },
479 },
480 },
481 "paths": {
482 "1": { "path": ["myc", "indicators"], "kind": "module" },
483 "2": { "path": ["myc", "indicators", "sma", "sma"], "kind": "function" },
484 },
485 })
486 }
487
488 #[test]
489 fn reexport_alias_resolves_alongside_canonical_path() {
490 let dir =
491 std::env::temp_dir().join(format!("soothfast-surface-test-{}", std::process::id()));
492 std::fs::create_dir_all(dir.join("src")).unwrap();
493 std::fs::write(dir.join("src/sma.rs"), "pub fn sma() {}\n").unwrap();
494
495 let doc = private_submodule_reexport_doc();
496 let surface = from_rustdoc(&doc, &dir);
497
498 assert!(surface.items.contains_key("myc::indicators::sma::sma"));
499 assert!(
500 surface.items.contains_key("myc::indicators::sma"),
501 "expected the flattened re-export path to resolve too: {:?}",
502 surface.items.keys().collect::<Vec<_>>()
503 );
504 let canonical = &surface.items["myc::indicators::sma::sma"];
505 let alias = &surface.items["myc::indicators::sma"];
506 assert_eq!(canonical.fingerprint, alias.fingerprint);
507 assert_eq!(alias.kind, "function");
508
509 let _ = std::fs::remove_dir_all(&dir);
510 }
511
512 #[test]
513 fn glob_reexport_aliases_every_child_by_its_own_name() {
514 let mut doc = private_submodule_reexport_doc();
516 doc["index"]["3"]["inner"]["use"] =
517 json!({ "source": "sma", "name": "sma", "id": 7, "is_glob": true });
518 doc["index"]["7"] = json!({
519 "visibility": "public",
520 "inner": { "module": { "items": [2] } },
521 });
522
523 let dir =
524 std::env::temp_dir().join(format!("soothfast-surface-glob-{}", std::process::id()));
525 std::fs::create_dir_all(dir.join("src")).unwrap();
526 std::fs::write(dir.join("src/sma.rs"), "pub fn sma() {}\n").unwrap();
527
528 let surface = from_rustdoc(&doc, &dir);
529 assert!(surface.items.contains_key("myc::indicators::sma::sma"));
530 assert!(surface.items.contains_key("myc::indicators::sma"));
531
532 let _ = std::fs::remove_dir_all(&dir);
533 }
534
535 fn impl_doc() -> Value {
536 let span =
537 |line: u64| json!({ "filename": "src/lib.rs", "begin": [line, 0], "end": [line, 0] });
538 json!({
539 "index": {
540 "1": { "visibility": "public", "inner": { "module": { "items": [2, 20] } } },
541 "2": {
542 "name": "Rewriter",
543 "visibility": "public",
544 "inner": { "struct": {} },
545 "span": span(1),
546 },
547 "3": {
548 "visibility": "default",
549 "inner": { "impl": {
550 "trait": null,
551 "for": { "resolved_path": { "id": 2 } },
552 "items": [4, 5, 6],
553 } },
554 },
555 "4": {
556 "name": "rewrite",
557 "visibility": "public",
558 "inner": { "function": {} },
559 "span": span(2),
560 },
561 "5": {
562 "name": "hidden",
563 "visibility": "default",
564 "inner": { "function": {} },
565 "span": span(3),
566 },
567 "6": {
568 "name": "LIMIT",
569 "visibility": "public",
570 "inner": { "assoc_const": {} },
571 "span": span(4),
572 },
573 "8": {
574 "visibility": "default",
575 "inner": { "impl": {
576 "trait": { "path": "Default" },
577 "for": { "resolved_path": { "id": 2 } },
578 "items": [7],
579 } },
580 },
581 "7": {
582 "name": "default",
583 "visibility": "public",
584 "inner": { "function": {} },
585 "span": span(5),
586 },
587 "20": {
588 "visibility": "public",
589 "inner": { "use": { "name": "Rewriter", "id": 2, "is_glob": false } },
590 },
591 },
592 "paths": {
593 "1": { "path": ["rw"], "kind": "module" },
594 "2": { "path": ["rw", "engine", "Rewriter"], "kind": "struct" },
595 },
596 })
597 }
598
599 #[test]
600 fn inherent_pub_members_resolve_under_the_type_path() {
601 let dir =
602 std::env::temp_dir().join(format!("soothfast-surface-impl-{}", std::process::id()));
603 std::fs::create_dir_all(dir.join("src")).unwrap();
604 std::fs::write(
605 dir.join("src/lib.rs"),
606 "pub struct Rewriter;\n pub fn rewrite() {}\n fn hidden() {}\n pub const LIMIT: u8 = 1;\n fn default() {}\n",
607 )
608 .unwrap();
609
610 let surface = from_rustdoc(&impl_doc(), &dir);
611
612 let method = surface.lookup("rw::engine::Rewriter::rewrite").unwrap();
613 assert_eq!(method.kind, "function");
614 assert_ne!(method.fingerprint, 0);
615 assert_eq!(
616 surface.lookup("rw::engine::Rewriter::LIMIT").unwrap().kind,
617 "constant"
618 );
619 assert!(surface.lookup("rw::engine::Rewriter::hidden").is_none());
620 assert!(surface.lookup("rw::engine::Rewriter::default").is_none());
621 assert!(surface.items.keys().all(|k| !k.ends_with("::rewrite")));
622 assert_eq!(
623 surface.lookup("rw::Rewriter::rewrite").unwrap().fingerprint,
624 method.fingerprint
625 );
626
627 let _ = std::fs::remove_dir_all(&dir);
628 }
629
630 fn item(kind: &str, fingerprint: u64) -> ItemInfo {
631 ItemInfo {
632 kind: kind.to_string(),
633 fingerprint,
634 signature: String::new(),
635 has_docs: false,
636 }
637 }
638
639 fn surface_of(items: Vec<(&str, ItemInfo)>) -> Surface {
640 Surface {
641 items: items.into_iter().map(|(p, i)| (p.to_string(), i)).collect(),
642 ..Surface::default()
643 }
644 }
645
646 #[test]
647 fn three_aliases_of_one_function_collapse_to_one_group() {
648 let surf = surface_of(vec![
649 ("myc::atr", item("function", 42)),
650 ("myc::indicators::atr", item("function", 42)),
651 ("myc::indicators::atr::atr", item("function", 42)),
652 ]);
653 let groups = surf.grouped();
654 assert_eq!(groups.len(), 1);
655 assert_eq!(groups[0].representative, "myc::atr");
656 assert_eq!(groups[0].aliases.len(), 3);
657 }
658
659 #[test]
660 fn covers_tag_on_a_longer_alias_still_counts_as_measured() {
661 let surf = surface_of(vec![
665 ("myc::atr", item("function", 42)),
666 ("myc::indicators::atr", item("function", 42)),
667 ]);
668 let g = &surf.grouped()[0];
669 assert!(
670 g.aliases
671 .iter()
672 .any(|a| a.as_str() == "myc::indicators::atr")
673 );
674 }
675
676 #[test]
677 fn distinct_functions_with_no_fingerprint_are_never_merged() {
678 let surf = surface_of(vec![
679 ("myc::a", item("function", 0)),
680 ("myc::b", item("function", 0)),
681 ]);
682 assert_eq!(surf.grouped().len(), 2);
683 }
684
685 #[test]
686 fn different_kinds_with_the_same_fingerprint_are_not_merged() {
687 let surf = surface_of(vec![
688 ("myc::a", item("function", 7)),
689 ("myc::A", item("struct", 7)),
690 ]);
691 assert_eq!(surf.grouped().len(), 2);
692 }
693
694 fn fingerprint_of(tag: &str, source: &str) -> (u64, String) {
695 fingerprint_of_kind(tag, "struct", source)
696 }
697
698 fn fingerprint_of_kind(tag: &str, kind: &str, source: &str) -> (u64, String) {
699 let dir = std::env::temp_dir().join(format!(
700 "soothfast-fingerprint-{}-{tag}",
701 std::process::id()
702 ));
703 std::fs::create_dir_all(&dir).unwrap();
704 std::fs::write(dir.join("lib.rs"), source).unwrap();
705 let span = json!({
706 "filename": "lib.rs",
707 "begin": [1, 0],
708 "end": [source.lines().count(), 0],
709 });
710 let got = span_fingerprint(&span, kind, &dir, &mut HashMap::new()).unwrap();
711 let _ = std::fs::remove_dir_all(&dir);
712 got
713 }
714
715 #[test]
716 fn a_comment_only_edit_does_not_move_the_fingerprint() {
717 let plain = fingerprint_of("plain", "pub struct S {\n a: u8,\n}\n");
718 let noted = fingerprint_of(
719 "noted",
720 "pub struct S {\n // widened for the wire format\n a: u8,\n}\n",
721 );
722 assert_eq!(plain.0, noted.0);
723 }
724
725 #[test]
726 fn a_field_change_still_moves_the_fingerprint() {
727 let before = fingerprint_of("before", "pub struct S {\n a: u8,\n}\n");
728 let after = fingerprint_of("after", "pub struct S {\n a: u16,\n}\n");
729 assert_ne!(before.0, after.0);
730 }
731
732 #[test]
733 fn a_struct_field_addition_changes_the_signature() {
734 let before = fingerprint_of_kind(
735 "sig-before",
736 "struct",
737 "pub struct S {\n pub a: u8,\n}\n",
738 );
739 let after = fingerprint_of_kind(
740 "sig-after",
741 "struct",
742 "pub struct S {\n pub a: u8,\n pub b: u8,\n}\n",
743 );
744 assert_ne!(before.1, after.1);
745 assert_eq!(after.1, "pub struct S { pub a: u8, pub b: u8 }");
746 }
747
748 #[test]
749 fn a_private_field_edit_is_body_only() {
750 let before = fingerprint_of_kind(
751 "priv-before",
752 "struct",
753 "pub struct S {\n pub a: u8,\n b: u8,\n}\n",
754 );
755 let after = fingerprint_of_kind(
756 "priv-after",
757 "struct",
758 "pub struct S {\n pub a: u8,\n b: u16,\n c: u8,\n}\n",
759 );
760 assert_ne!(before.0, after.0);
761 assert_eq!(before.1, after.1);
762 assert_eq!(before.1, "pub struct S { pub a: u8 }");
763 }
764
765 #[test]
766 fn generic_fields_and_attributes_are_read_as_members() {
767 let got = fingerprint_of_kind(
768 "generic",
769 "struct",
770 "pub struct S {\n #[serde(rename = \"m\")]\n pub map: HashMap<String, Vec<u8>>,\n pub(crate) hidden: u8,\n}\n",
771 );
772 assert_eq!(got.1, "pub struct S { pub map: HashMap<String, Vec<u8>> }");
773 }
774
775 #[test]
776 fn a_documented_public_field_counts_as_signature() {
777 let before = fingerprint_of_kind(
778 "pubdoc-before",
779 "struct",
780 "pub struct S {\n /// count\n pub a: u8,\n}\n",
781 );
782 let after = fingerprint_of_kind(
783 "pubdoc-after",
784 "struct",
785 "pub struct S {\n /// count\n pub a: u8,\n /// total\n pub b: u8,\n}\n",
786 );
787 assert_ne!(before.1, after.1);
788 assert_eq!(after.1, "pub struct S { pub a: u8, pub b: u8 }");
789 }
790
791 #[test]
792 fn a_variant_doc_edit_moves_the_fingerprint_but_not_the_signature() {
793 let before = fingerprint_of_kind(
794 "vdoc-before",
795 "enum",
796 "pub enum E {\n /// first\n A,\n B,\n}\n",
797 );
798 let after = fingerprint_of_kind(
799 "vdoc-after",
800 "enum",
801 "pub enum E {\n /// the first\n A,\n B,\n}\n",
802 );
803 assert_ne!(before.0, after.0);
804 assert_eq!(before.1, after.1);
805 assert_eq!(before.1, "pub enum E { A, B }");
806 }
807
808 #[test]
809 fn a_function_pointer_field_does_not_swallow_its_neighbours() {
810 let got = fingerprint_of_kind(
811 "fnptr",
812 "struct",
813 "pub struct S {\n pub f: fn(u8) -> u8,\n pub g: u8,\n}\n",
814 );
815 assert_eq!(got.1, "pub struct S { pub f: fn(u8) -> u8, pub g: u8 }");
816 }
817
818 #[test]
819 fn an_enum_variant_addition_changes_the_signature() {
820 let before = fingerprint_of_kind("var-before", "enum", "pub enum E {\n A,\n}\n");
821 let after = fingerprint_of_kind("var-after", "enum", "pub enum E {\n A,\n B,\n}\n");
822 assert_ne!(before.1, after.1);
823 }
824
825 #[test]
826 fn a_function_body_edit_keeps_its_signature() {
827 let before = fingerprint_of_kind("fn-before", "function", "pub fn f() -> u8 {\n 1\n}\n");
828 let after = fingerprint_of_kind("fn-after", "function", "pub fn f() -> u8 {\n 2\n}\n");
829 assert_ne!(before.0, after.0);
830 assert_eq!(before.1, after.1);
831 assert_eq!(before.1, "pub fn f() -> u8");
832 }
833
834 #[test]
835 fn a_doc_comment_on_a_field_is_part_of_the_fingerprint() {
836 let before = fingerprint_of(
837 "doc-before",
838 "pub struct S {\n /// count\n a: u8,\n}\n",
839 );
840 let after = fingerprint_of(
841 "doc-after",
842 "pub struct S {\n /// total\n a: u8,\n}\n",
843 );
844 assert_ne!(before.0, after.0);
845 }
846}