1use std::collections::{BTreeMap, BTreeSet};
24
25use serde::{Deserialize, Serialize};
26
27use crate::env::std_domain::{source_path_of, DocumentIds, DocumentSource};
28use crate::env::BuildEnvironment;
29use crate::error::{BuildWarning, WarningType};
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct PyObjectEntry {
35 pub docname: String,
36 pub node_id: String,
37 pub objtype: String,
38 pub aliased: bool,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct PyModuleEntry {
48 pub docname: String,
49 pub node_id: String,
50 pub synopsis: String,
51 pub platform: String,
52 pub deprecated: bool,
53}
54
55#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
74pub struct PyDomainData {
75 pub objects: Vec<(String, PyObjectEntry)>,
77 pub objects_index: BTreeMap<String, usize>,
79 pub modules: Vec<(String, PyModuleEntry)>,
81 pub modules_index: BTreeMap<String, usize>,
83}
84
85impl PyDomainData {
86 pub fn note_object(&mut self, name: &str, entry: PyObjectEntry) -> Option<String> {
99 if let Some(&index) = self.objects_index.get(name) {
100 let other = &self.objects[index].1;
101 if !other.aliased && entry.aliased {
102 return None;
105 }
106 let warn = (other.aliased == entry.aliased).then(|| other.docname.clone());
110 self.objects[index].1 = entry;
111 warn
112 } else {
113 self.objects_index
114 .insert(name.to_string(), self.objects.len());
115 self.objects.push((name.to_string(), entry));
116 None
117 }
118 }
119
120 pub fn note_module(&mut self, name: &str, entry: PyModuleEntry) {
126 if let Some(&index) = self.modules_index.get(name) {
127 self.modules[index].1 = entry;
128 } else {
129 self.modules_index
130 .insert(name.to_string(), self.modules.len());
131 self.modules.push((name.to_string(), entry));
132 }
133 }
134
135 pub fn clear_doc(&mut self, docname: &str) {
140 self.objects.retain(|(_, entry)| entry.docname != docname);
141 self.modules.retain(|(_, entry)| entry.docname != docname);
142 self.rebuild_indices();
143 }
144
145 pub fn merge(&mut self, other: &PyDomainData, docnames: &BTreeSet<String>) {
151 for (name, entry) in &other.objects {
152 if !docnames.contains(&entry.docname) {
153 continue;
154 }
155 if let Some(&index) = self.objects_index.get(name) {
156 self.objects[index].1 = entry.clone();
157 } else {
158 self.objects_index.insert(name.clone(), self.objects.len());
159 self.objects.push((name.clone(), entry.clone()));
160 }
161 }
162 for (name, entry) in &other.modules {
163 if !docnames.contains(&entry.docname) {
164 continue;
165 }
166 if let Some(&index) = self.modules_index.get(name) {
167 self.modules[index].1 = entry.clone();
168 } else {
169 self.modules_index.insert(name.clone(), self.modules.len());
170 self.modules.push((name.clone(), entry.clone()));
171 }
172 }
173 }
174
175 fn rebuild_indices(&mut self) {
176 self.objects_index = self
177 .objects
178 .iter()
179 .enumerate()
180 .map(|(index, (name, _))| (name.clone(), index))
181 .collect();
182 self.modules_index = self
183 .modules
184 .iter()
185 .enumerate()
186 .map(|(index, (name, _))| (name.clone(), index))
187 .collect();
188 }
189}
190
191const OBJECT_TYPES: &[&str] = &[
199 "function",
200 "data",
201 "class",
202 "exception",
203 "method",
204 "classmethod",
205 "staticmethod",
206 "attribute",
207 "property",
208 "type",
209 "module",
210];
211
212pub(crate) fn objtypes_for_role(role: &str) -> Option<&'static [&'static str]> {
220 Some(match role {
221 "func" => &["function"],
222 "data" => &["data"],
223 "class" => &["class", "exception", "type"],
224 "exc" => &["class", "exception"],
225 "meth" => &["method", "classmethod", "staticmethod"],
226 "attr" => &["attribute", "property"],
227 "_prop" => &["property"],
230 "type" => &["type"],
231 "mod" => &["module"],
232 "obj" => OBJECT_TYPES,
233 _ => return None,
234 })
235}
236
237pub(crate) fn role_for_objtype(objtype: &str) -> Option<&'static str> {
242 Some(match objtype {
243 "function" => "func",
244 "data" => "data",
245 "class" => "class",
246 "exception" => "exc",
247 "method" | "classmethod" | "staticmethod" => "meth",
248 "attribute" | "property" => "attr",
249 "type" => "type",
250 "module" => "mod",
251 _ => return None,
252 })
253}
254
255pub fn find_obj<'a>(
274 data: &'a PyDomainData,
275 modname: Option<&str>,
276 classname: Option<&str>,
277 name: &str,
278 typ: Option<&str>,
279 searchmode: u8,
280) -> Vec<(String, &'a PyObjectEntry)> {
281 let name = name.strip_suffix("()").unwrap_or(name);
283 if name.is_empty() {
284 return Vec::new();
285 }
286 let modname = modname.filter(|m| !m.is_empty());
288 let classname = classname.filter(|c| !c.is_empty());
289
290 let entry_of = |fullname: &str| {
291 data.objects_index
292 .get(fullname)
293 .map(|&index| &data.objects[index].1)
294 };
295
296 let newname: Option<String> = if searchmode == 1 {
297 let objtypes = match typ {
298 None => Some(OBJECT_TYPES),
299 Some(role) => objtypes_for_role(role),
300 };
301 let Some(objtypes) = objtypes else {
302 return Vec::new();
304 };
305 let gated = |fullname: &str| {
306 entry_of(fullname).is_some_and(|entry| objtypes.contains(&entry.objtype.as_str()))
307 };
308 let qualified = match (modname, classname) {
309 (Some(modname), Some(classname)) => {
310 Some(format!("{modname}.{classname}.{name}")).filter(|fullname| gated(fullname))
311 }
312 _ => None,
313 };
314 if qualified.is_some() {
315 qualified
316 } else if let Some(dotted) = modname
317 .map(|modname| format!("{modname}.{name}"))
318 .filter(|dotted| gated(dotted))
319 {
320 Some(dotted)
321 } else if gated(name) {
322 Some(name.to_string())
323 } else {
324 let searchname = format!(".{name}");
327 return data
328 .objects
329 .iter()
330 .filter(|(oname, entry)| {
331 oname.ends_with(&searchname) && objtypes.contains(&entry.objtype.as_str())
332 })
333 .map(|(oname, entry)| (oname.clone(), entry))
334 .collect();
335 }
336 } else {
337 if entry_of(name).is_some() {
339 Some(name.to_string())
340 } else if typ == Some("mod") {
341 return Vec::new();
343 } else {
344 [
345 classname.map(|classname| format!("{classname}.{name}")),
346 modname.map(|modname| format!("{modname}.{name}")),
347 match (modname, classname) {
348 (Some(modname), Some(classname)) => {
349 Some(format!("{modname}.{classname}.{name}"))
350 }
351 _ => None,
352 },
353 ]
354 .into_iter()
355 .flatten()
356 .find(|candidate| entry_of(candidate).is_some())
357 }
358 };
359 newname
360 .map(|newname| {
361 let entry = entry_of(&newname).expect("candidate was just found");
362 vec![(newname, entry)]
363 })
364 .unwrap_or_default()
365}
366
367#[derive(Debug, PartialEq)]
370pub struct PyXrefTarget<'a> {
371 pub docname: &'a str,
372 pub node_id: &'a str,
373 pub reftitle: String,
379 pub is_module: bool,
383}
384
385pub fn resolve_xref<'a>(
392 data: &'a PyDomainData,
393 modname: Option<&str>,
394 classname: Option<&str>,
395 reftype: &str,
396 target: &str,
397 searchmode: u8,
398) -> (Option<PyXrefTarget<'a>>, Option<String>) {
399 let retry = |typ: &str| find_obj(data, modname, classname, target, Some(typ), searchmode);
400 let mut matches = retry(reftype);
401 if matches.is_empty() && reftype == "class" {
402 matches = retry("data");
404 if matches.is_empty() {
405 matches = retry("attr");
406 }
407 }
408 if matches.is_empty() && reftype == "attr" {
409 matches = retry("meth");
411 }
412 if matches.is_empty() && reftype == "meth" {
413 matches = retry("_prop");
415 }
416
417 if matches.is_empty() {
418 return (None, None);
419 }
420 let mut warning = None;
421 let (name, entry) = if matches.len() > 1 {
422 let canonicals: Vec<&(String, &PyObjectEntry)> =
423 matches.iter().filter(|(_, entry)| !entry.aliased).collect();
424 if canonicals.len() == 1 {
425 let (name, entry) = canonicals[0];
427 (name.clone(), *entry)
428 } else {
429 warning = Some(format!(
430 "more than one target found for cross-reference {}: {}",
431 crate::utils::py_repr_str(target),
432 matches
433 .iter()
434 .map(|(name, _)| name.as_str())
435 .collect::<Vec<_>>()
436 .join(", ")
437 ));
438 let (name, entry) = &matches[0];
440 (name.clone(), *entry)
441 }
442 } else {
443 let (name, entry) = matches.remove(0);
444 (name, entry)
445 };
446
447 if entry.objtype == "module" {
448 (module_xref_target(data, name), warning)
449 } else {
450 (
451 Some(PyXrefTarget {
452 docname: &entry.docname,
453 node_id: &entry.node_id,
454 reftitle: name,
455 is_module: false,
456 }),
457 warning,
458 )
459 }
460}
461
462fn module_xref_target(data: &PyDomainData, name: String) -> Option<PyXrefTarget<'_>> {
472 let &index = data.modules_index.get(&name)?;
473 let module = &data.modules[index].1;
474 let mut reftitle = name;
475 if !module.synopsis.is_empty() {
476 reftitle.push_str(": ");
477 reftitle.push_str(&module.synopsis);
478 }
479 if module.deprecated {
480 reftitle.push_str(" (deprecated)");
481 }
482 if !module.platform.is_empty() {
483 reftitle.push_str(" (");
484 reftitle.push_str(&module.platform);
485 reftitle.push(')');
486 }
487 Some(PyXrefTarget {
488 docname: &module.docname,
489 node_id: &module.node_id,
490 reftitle,
491 is_module: true,
492 })
493}
494
495pub fn resolve_any_xref<'a>(
503 data: &'a PyDomainData,
504 modname: Option<&str>,
505 classname: Option<&str>,
506 target: &str,
507) -> Vec<(String, PyXrefTarget<'a>)> {
508 let matches = find_obj(data, modname, classname, target, None, 1);
509 let multiple = matches.len() > 1;
510 let mut results = Vec::new();
511 for (name, entry) in matches {
512 if multiple && entry.aliased {
513 continue;
515 }
516 if entry.objtype == "module" {
517 if let Some(target) = module_xref_target(data, name) {
518 results.push(("py:mod".to_string(), target));
519 }
520 } else if let Some(role) = role_for_objtype(&entry.objtype) {
521 results.push((
522 format!("py:{role}"),
523 PyXrefTarget {
524 docname: &entry.docname,
525 node_id: &entry.node_id,
526 reftitle: name,
527 is_module: false,
528 },
529 ));
530 }
531 }
532 results
533}
534
535#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
544pub struct ModindexEntry {
545 pub name: String,
546 pub subtype: u8,
547 pub docname: String,
548 pub anchor: String,
549 pub extra: String,
550 pub qualifier: String,
551 pub descr: String,
552}
553
554#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
556pub struct ModindexGroup {
557 pub letter: String,
558 pub entries: Vec<ModindexEntry>,
559}
560
561#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
563pub struct PyModindex {
564 pub groups: Vec<ModindexGroup>,
565 pub collapse: bool,
566}
567
568pub fn generate_modindex(data: &PyDomainData, common_prefix: &[String]) -> PyModindex {
581 let mut ignores: Vec<&str> = common_prefix.iter().map(String::as_str).collect();
582 ignores.sort_by_key(|prefix| std::cmp::Reverse(prefix.len()));
583
584 let mut modules: Vec<(&str, &PyModuleEntry)> = data
585 .modules
586 .iter()
587 .map(|(name, entry)| (name.as_str(), entry))
588 .collect();
589 modules.sort_by_key(|(name, _)| name.to_lowercase());
590
591 let mut content: BTreeMap<String, Vec<ModindexEntry>> = BTreeMap::new();
592 let mut prev_modname = String::new();
593 let mut num_top_levels = 0usize;
594 for (full_name, module) in &modules {
595 let mut modname = *full_name;
596 let mut stripped = "";
597 for ignore in &ignores {
598 if let Some(rest) = modname.strip_prefix(ignore) {
599 modname = rest;
600 stripped = ignore;
601 break;
602 }
603 }
604 if modname.is_empty() {
606 (modname, stripped) = (stripped, "");
607 }
608
609 let Some(first) = modname.chars().next() else {
613 continue;
614 };
615 let entries = content
616 .entry(first.to_lowercase().collect::<String>())
617 .or_default();
618
619 let package = modname.split('.').next().unwrap_or(modname);
620 let subtype = if package != modname {
621 if prev_modname == package {
623 if let Some(last) = entries.last_mut() {
625 last.subtype = 1;
626 }
627 } else if !prev_modname.starts_with(package) {
628 entries.push(ModindexEntry {
630 name: format!("{stripped}{package}"),
631 subtype: 1,
632 docname: String::new(),
633 anchor: String::new(),
634 extra: String::new(),
635 qualifier: String::new(),
636 descr: String::new(),
637 });
638 }
639 2
640 } else {
641 num_top_levels += 1;
642 0
643 };
644
645 entries.push(ModindexEntry {
646 name: format!("{stripped}{modname}"),
647 subtype,
648 docname: module.docname.clone(),
649 anchor: module.node_id.clone(),
650 extra: module.platform.clone(),
651 qualifier: if module.deprecated {
652 "Deprecated".to_string()
653 } else {
654 String::new()
655 },
656 descr: module.synopsis.clone(),
657 });
658 prev_modname = modname.to_string();
659 }
660
661 let collapse = modules.len() - num_top_levels < num_top_levels;
664
665 PyModindex {
666 groups: content
669 .into_iter()
670 .map(|(letter, entries)| ModindexGroup { letter, entries })
671 .collect(),
672 collapse,
673 }
674}
675
676pub fn modindex_snapshot(modindex: &PyModindex) -> serde_json::Value {
679 serde_json::to_value(modindex).unwrap_or(serde_json::Value::Null)
680}
681
682const BUILTIN_CLASSES: &[&str] = &[
689 "ArithmeticError",
690 "AssertionError",
691 "AttributeError",
692 "BaseException",
693 "BaseExceptionGroup",
694 "BlockingIOError",
695 "BrokenPipeError",
696 "BufferError",
697 "BytesWarning",
698 "ChildProcessError",
699 "ConnectionAbortedError",
700 "ConnectionError",
701 "ConnectionRefusedError",
702 "ConnectionResetError",
703 "DeprecationWarning",
704 "EOFError",
705 "EncodingWarning",
706 "EnvironmentError",
707 "Exception",
708 "ExceptionGroup",
709 "FileExistsError",
710 "FileNotFoundError",
711 "FloatingPointError",
712 "FutureWarning",
713 "GeneratorExit",
714 "IOError",
715 "ImportError",
716 "ImportWarning",
717 "IndentationError",
718 "IndexError",
719 "InterruptedError",
720 "IsADirectoryError",
721 "KeyError",
722 "KeyboardInterrupt",
723 "LookupError",
724 "MemoryError",
725 "ModuleNotFoundError",
726 "NameError",
727 "NotADirectoryError",
728 "NotImplementedError",
729 "OSError",
730 "OverflowError",
731 "PendingDeprecationWarning",
732 "PermissionError",
733 "ProcessLookupError",
734 "RecursionError",
735 "ReferenceError",
736 "ResourceWarning",
737 "RuntimeError",
738 "RuntimeWarning",
739 "StopAsyncIteration",
740 "StopIteration",
741 "SyntaxError",
742 "SyntaxWarning",
743 "SystemError",
744 "SystemExit",
745 "TabError",
746 "TimeoutError",
747 "TypeError",
748 "UnboundLocalError",
749 "UnicodeDecodeError",
750 "UnicodeEncodeError",
751 "UnicodeError",
752 "UnicodeTranslateError",
753 "UnicodeWarning",
754 "UserWarning",
755 "ValueError",
756 "Warning",
757 "ZeroDivisionError",
758 "__loader__",
759 "bool",
760 "bytearray",
761 "bytes",
762 "classmethod",
763 "complex",
764 "dict",
765 "enumerate",
766 "filter",
767 "float",
768 "frozenset",
769 "int",
770 "list",
771 "map",
772 "memoryview",
773 "object",
774 "property",
775 "range",
776 "reversed",
777 "set",
778 "slice",
779 "staticmethod",
780 "str",
781 "super",
782 "tuple",
783 "type",
784 "zip",
785];
786
787const TYPING_ALL: &[&str] = &[
790 "AbstractSet",
791 "Annotated",
792 "Any",
793 "AnyStr",
794 "AsyncContextManager",
795 "AsyncGenerator",
796 "AsyncIterable",
797 "AsyncIterator",
798 "Awaitable",
799 "BinaryIO",
800 "ByteString",
801 "Callable",
802 "ChainMap",
803 "ClassVar",
804 "Collection",
805 "Concatenate",
806 "Container",
807 "ContextManager",
808 "Coroutine",
809 "Counter",
810 "DefaultDict",
811 "Deque",
812 "Dict",
813 "Final",
814 "ForwardRef",
815 "FrozenSet",
816 "Generator",
817 "Generic",
818 "Hashable",
819 "IO",
820 "ItemsView",
821 "Iterable",
822 "Iterator",
823 "KeysView",
824 "List",
825 "Literal",
826 "LiteralString",
827 "Mapping",
828 "MappingView",
829 "Match",
830 "MutableMapping",
831 "MutableSequence",
832 "MutableSet",
833 "NamedTuple",
834 "Never",
835 "NewType",
836 "NoReturn",
837 "NotRequired",
838 "Optional",
839 "OrderedDict",
840 "ParamSpec",
841 "ParamSpecArgs",
842 "ParamSpecKwargs",
843 "Pattern",
844 "Protocol",
845 "Required",
846 "Reversible",
847 "Self",
848 "Sequence",
849 "Set",
850 "Sized",
851 "SupportsAbs",
852 "SupportsBytes",
853 "SupportsComplex",
854 "SupportsFloat",
855 "SupportsIndex",
856 "SupportsInt",
857 "SupportsRound",
858 "TYPE_CHECKING",
859 "Text",
860 "TextIO",
861 "Tuple",
862 "Type",
863 "TypeAlias",
864 "TypeAliasType",
865 "TypeGuard",
866 "TypeVar",
867 "TypeVarTuple",
868 "TypedDict",
869 "Union",
870 "Unpack",
871 "ValuesView",
872 "assert_never",
873 "assert_type",
874 "cast",
875 "clear_overloads",
876 "dataclass_transform",
877 "final",
878 "get_args",
879 "get_origin",
880 "get_overloads",
881 "get_type_hints",
882 "is_typeddict",
883 "no_type_check",
884 "no_type_check_decorator",
885 "overload",
886 "override",
887 "reveal_type",
888 "runtime_checkable",
889];
890
891pub fn builtin_resolver(reftype: &str, target: &str) -> bool {
904 match reftype {
905 "class" | "obj" if target == "None" => true,
906 "class" | "obj" | "exc" => {
907 BUILTIN_CLASSES.binary_search(&target).is_ok()
908 || TYPING_ALL
909 .binary_search(&target.strip_prefix("typing.").unwrap_or(target))
910 .is_ok()
911 }
912 _ => false,
913 }
914}
915
916pub(crate) fn collect_registrations(
933 env: &mut BuildEnvironment,
934 doc: &DocumentSource<'_>,
935 ids: &DocumentIds<'_>,
936 warnings: &mut Vec<(usize, BuildWarning)>,
937) {
938 for record in &doc.registry.py_modules {
939 env.py.note_module(
940 &record.name,
941 PyModuleEntry {
942 docname: doc.docname.to_string(),
943 node_id: record.node_id.clone(),
944 synopsis: record.synopsis.clone(),
945 platform: record.platform.clone(),
946 deprecated: record.deprecated,
947 },
948 );
949 }
950 for record in &doc.registry.py_objects {
951 let Some(other) = env.py.note_object(
952 &record.fullname,
953 PyObjectEntry {
954 docname: doc.docname.to_string(),
955 node_id: record.node_id.clone(),
956 objtype: record.objtype.clone(),
957 aliased: record.aliased,
958 },
959 ) else {
960 continue;
961 };
962 let order = ids
963 .get(&record.node_id)
964 .map(|(order, _)| order)
965 .unwrap_or(usize::MAX);
966 warnings.push((
967 order,
968 BuildWarning::new(
972 source_path_of(doc, record.source),
973 Some(record.lineno as usize),
974 format!(
975 "duplicate object description of {}, other instance in {}, \
976 use :no-index: for one of them",
977 record.fullname, other
978 ),
979 WarningType::DuplicateLabel,
980 )
981 .with_category(None),
982 ));
983 }
984}
985
986#[cfg(test)]
987mod tests {
988 use super::*;
989 use crate::env::std_domain;
990 use crate::rst::{parse_rst_full, ParseOptions};
991 use std::path::PathBuf;
992
993 fn entry(docname: &str, node_id: &str, objtype: &str, aliased: bool) -> PyObjectEntry {
994 PyObjectEntry {
995 docname: docname.to_string(),
996 node_id: node_id.to_string(),
997 objtype: objtype.to_string(),
998 aliased,
999 }
1000 }
1001
1002 fn module_entry(docname: &str, node_id: &str) -> PyModuleEntry {
1003 PyModuleEntry {
1004 docname: docname.to_string(),
1005 node_id: node_id.to_string(),
1006 synopsis: String::new(),
1007 platform: String::new(),
1008 deprecated: false,
1009 }
1010 }
1011
1012 fn object_rows(data: &PyDomainData) -> Vec<(&str, &str, bool)> {
1014 data.objects
1015 .iter()
1016 .map(|(name, e)| (name.as_str(), e.docname.as_str(), e.aliased))
1017 .collect()
1018 }
1019
1020 fn assert_indices_consistent(data: &PyDomainData) {
1021 assert_eq!(data.objects_index.len(), data.objects.len());
1022 for (name, &index) in &data.objects_index {
1023 assert_eq!(&data.objects[index].0, name, "objects_index[{name}]");
1024 }
1025 assert_eq!(data.modules_index.len(), data.modules.len());
1026 for (name, &index) in &data.modules_index {
1027 assert_eq!(&data.modules[index].0, name, "modules_index[{name}]");
1028 }
1029 }
1030
1031 fn data_of(entries: &[(&str, &str)]) -> PyDomainData {
1036 let mut data = PyDomainData::default();
1037 for (name, objtype) in entries {
1038 data.note_object(name, entry("index", name, objtype, false));
1039 }
1040 data
1041 }
1042
1043 fn names(matches: &[(String, &PyObjectEntry)]) -> Vec<String> {
1044 matches.iter().map(|(name, _)| name.clone()).collect()
1045 }
1046
1047 #[test]
1050 fn exact_mode_walks_the_candidate_chain_in_spec_order() {
1051 let data = data_of(&[
1052 ("m.C.x", "method"),
1053 ("m.x", "function"),
1054 ("C.x", "method"),
1055 ("x", "function"),
1056 ]);
1057 let find = |modname: Option<&str>, classname: Option<&str>| {
1058 names(&find_obj(&data, modname, classname, "x", Some("func"), 0))
1059 };
1060 assert_eq!(find(Some("m"), Some("C")), vec!["x"], "bare name first");
1061 let partial = data_of(&[("m.C.x", "method"), ("m.x", "function"), ("C.x", "method")]);
1062 assert_eq!(
1063 names(&find_obj(
1064 &partial,
1065 Some("m"),
1066 Some("C"),
1067 "x",
1068 Some("func"),
1069 0
1070 )),
1071 vec!["C.x"],
1072 "then classname.name"
1073 );
1074 let partial = data_of(&[("m.C.x", "method"), ("m.x", "function")]);
1075 assert_eq!(
1076 names(&find_obj(
1077 &partial,
1078 Some("m"),
1079 Some("C"),
1080 "x",
1081 Some("func"),
1082 0
1083 )),
1084 vec!["m.x"],
1085 "then modname.name"
1086 );
1087 let partial = data_of(&[("m.C.x", "method")]);
1088 assert_eq!(
1089 names(&find_obj(
1090 &partial,
1091 Some("m"),
1092 Some("C"),
1093 "x",
1094 Some("func"),
1095 0
1096 )),
1097 vec!["m.C.x"],
1098 "then modname.classname.name"
1099 );
1100 assert!(
1101 find_obj(&partial, None, None, "x", Some("func"), 0).is_empty(),
1102 "no context, no prefix candidates"
1103 );
1104 }
1105
1106 #[test]
1109 fn exact_mode_ignores_the_object_type() {
1110 let data = data_of(&[("thing", "class")]);
1111 assert_eq!(
1112 names(&find_obj(&data, None, None, "thing", Some("func"), 0)),
1113 vec!["thing"]
1114 );
1115 }
1116
1117 #[test]
1122 fn mod_takes_only_the_bare_name_match() {
1123 let data = data_of(&[("pkg.sub", "module")]);
1124 assert!(find_obj(&data, Some("pkg"), None, "sub", Some("mod"), 0).is_empty());
1125 let shadowed = data_of(&[("sub", "function")]);
1126 assert_eq!(
1127 names(&find_obj(
1128 &shadowed,
1129 Some("pkg"),
1130 None,
1131 "sub",
1132 Some("mod"),
1133 0
1134 )),
1135 vec!["sub"],
1136 "the bare-name arm runs before the mod cutoff and skips no types"
1137 );
1138 }
1139
1140 #[test]
1143 fn trailing_parens_are_stripped_before_any_lookup() {
1144 let data = data_of(&[("m.f", "function")]);
1145 assert_eq!(
1146 names(&find_obj(&data, Some("m"), None, "f()", Some("obj"), 0)),
1147 vec!["m.f"],
1148 "exact mode"
1149 );
1150 assert_eq!(
1151 names(&find_obj(&data, None, None, "f()", Some("obj"), 1)),
1152 vec!["m.f"],
1153 "refspecific mode (the fuzzy pass sees the stripped name)"
1154 );
1155 assert!(
1156 find_obj(&data, Some("m"), None, "()", Some("obj"), 0).is_empty(),
1157 "a name that is nothing but parens strips to empty and matches nothing"
1158 );
1159 }
1160
1161 #[test]
1164 fn refspecific_mode_prefers_the_most_qualified_gated_candidate() {
1165 let data = data_of(&[
1166 ("meth", "function"),
1167 ("m.meth", "function"),
1168 ("m.C.meth", "method"),
1169 ]);
1170 assert_eq!(
1171 names(&find_obj(
1172 &data,
1173 Some("m"),
1174 Some("C"),
1175 "meth",
1176 Some("meth"),
1177 1
1178 )),
1179 vec!["m.C.meth"],
1180 "most qualified first"
1181 );
1182 assert_eq!(
1183 names(&find_obj(
1184 &data,
1185 Some("m"),
1186 Some("C"),
1187 "meth",
1188 Some("func"),
1189 1
1190 )),
1191 vec!["m.meth"],
1192 "the objtype gate skips m.C.meth for :func: and lands on m.meth"
1193 );
1194 assert_eq!(
1195 names(&find_obj(&data, None, None, "meth", Some("func"), 1)),
1196 vec!["meth"],
1197 "no context leaves the bare-name candidate"
1198 );
1199 }
1200
1201 #[test]
1204 fn the_fuzzy_pass_is_gated_and_registration_ordered() {
1205 let data = data_of(&[
1206 ("zeta.same", "function"),
1207 ("alpha.same", "function"),
1208 ("beta.same", "class"),
1209 ]);
1210 assert_eq!(
1211 names(&find_obj(&data, None, None, "same", Some("func"), 1)),
1212 vec!["zeta.same", "alpha.same"],
1213 "registration order, objtype-filtered (beta.same is a class)"
1214 );
1215 let with_exact = data_of(&[("zeta.same", "function"), ("same", "function")]);
1216 assert_eq!(
1217 names(&find_obj(&with_exact, None, None, "same", Some("func"), 1)),
1218 vec!["same"],
1219 "an exact bare-name hit suppresses the fuzzy pass"
1220 );
1221 assert!(
1222 find_obj(&data, None, None, "ame", Some("func"), 1).is_empty(),
1223 "the scan matches '.name', never a bare substring"
1224 );
1225 }
1226
1227 #[test]
1232 fn roles_without_objtypes_match_nothing_in_refspecific_mode() {
1233 let data = data_of(&[("pkg.mydeco", "function")]);
1234 assert!(find_obj(&data, None, None, "mydeco", Some("deco"), 1).is_empty());
1235 assert_eq!(
1236 names(&find_obj(&data, None, None, "pkg.mydeco", Some("deco"), 0)),
1237 vec!["pkg.mydeco"]
1238 );
1239 }
1240
1241 #[test]
1243 fn a_none_type_searches_all_object_types() {
1244 let data = data_of(&[("m.thing", "attribute")]);
1245 assert_eq!(
1246 names(&find_obj(&data, None, None, "thing", None, 1)),
1247 vec!["m.thing"]
1248 );
1249 }
1250
1251 #[test]
1255 fn resolve_xref_walks_the_type_fallback_chains() {
1256 let alias = data_of(&[("Alias", "data")]);
1257 let (found, warning) = resolve_xref(&alias, None, None, "class", "Alias", 0);
1258 assert_eq!(warning, None);
1259 assert_eq!(
1260 found,
1261 Some(PyXrefTarget {
1262 docname: "index",
1263 node_id: "Alias",
1264 reftitle: "Alias".to_string(),
1265 is_module: false,
1266 }),
1267 "a type alias documented as data resolves through :class:"
1268 );
1269
1270 let attr_alias = data_of(&[("A.x", "attribute")]);
1271 let (found, _) = resolve_xref(&attr_alias, None, Some("A"), "class", "x", 0);
1272 assert!(found.is_some(), "class falls back to attr after data");
1273
1274 let prop = data_of(&[("K.oldm", "method"), ("K.prop", "property")]);
1275 let (found, _) = resolve_xref(&prop, None, Some("K"), "attr", "oldm", 0);
1276 assert_eq!(found.unwrap().node_id, "K.oldm", "attr falls back to meth");
1277 let (found, _) = resolve_xref(&prop, None, Some("K"), "meth", "prop", 0);
1278 assert_eq!(
1279 found.unwrap().node_id,
1280 "K.prop",
1281 "meth falls back to property via the secret _prop role"
1282 );
1283 }
1284
1285 #[test]
1289 fn ambiguity_warns_with_candidates_in_registration_order_and_takes_the_first() {
1290 let data = data_of(&[("zeta.same", "function"), ("alpha.same", "function")]);
1291 let (found, warning) = resolve_xref(&data, None, None, "func", "same", 1);
1292 assert_eq!(
1293 warning.as_deref(),
1294 Some("more than one target found for cross-reference 'same': zeta.same, alpha.same")
1295 );
1296 assert_eq!(found.unwrap().node_id, "zeta.same");
1297 }
1298
1299 #[test]
1302 fn a_single_non_aliased_match_wins_silently() {
1303 let mut data = PyDomainData::default();
1304 data.note_object("alpha.f", entry("index", "alpha.f", "function", false));
1305 data.note_object("beta.f", entry("index", "alpha.f", "function", true));
1306 let (found, warning) = resolve_xref(&data, None, None, "func", "f", 1);
1307 assert_eq!(warning, None);
1308 assert_eq!(found.unwrap().reftitle, "alpha.f");
1309 }
1310
1311 #[test]
1316 fn a_module_target_carries_the_full_reftitle() {
1317 let mut data = PyDomainData::default();
1318 data.note_object("both", entry("index", "module-both", "module", false));
1319 data.note_module(
1320 "both",
1321 PyModuleEntry {
1322 docname: "index".to_string(),
1323 node_id: "module-both".to_string(),
1324 synopsis: "Some synopsis.".to_string(),
1325 platform: "Unix, Windows".to_string(),
1326 deprecated: true,
1327 },
1328 );
1329 let (found, _) = resolve_xref(&data, None, None, "mod", "both", 0);
1330 assert_eq!(
1331 found,
1332 Some(PyXrefTarget {
1333 docname: "index",
1334 node_id: "module-both",
1335 reftitle: "both: Some synopsis. (deprecated) (Unix, Windows)".to_string(),
1336 is_module: true,
1337 })
1338 );
1339 }
1340
1341 #[test]
1347 fn resolve_any_finds_functions_and_modules_with_their_roles() {
1348 let mut data = PyDomainData::default();
1349 data.note_object("m", entry("index", "module-m", "module", false));
1350 data.note_module("m", module_entry("index", "module-m"));
1351 data.note_object("m.f", entry("index", "m.f", "function", false));
1352
1353 let f = resolve_any_xref(&data, Some("m"), None, "f");
1354 assert_eq!(f.len(), 1);
1355 assert_eq!(f[0].0, "py:func");
1356 assert_eq!(f[0].1.reftitle, "m.f");
1357 assert!(!f[0].1.is_module);
1358
1359 let m = resolve_any_xref(&data, Some("m"), None, "m");
1360 assert_eq!(m.len(), 1);
1361 assert_eq!(m[0].0, "py:mod");
1362 assert_eq!(m[0].1.node_id, "module-m");
1363 assert!(m[0].1.is_module);
1364
1365 let parens = resolve_any_xref(&data, Some("m"), None, "f()");
1367 assert_eq!(parens.len(), 1);
1368 assert_eq!(parens[0].1.reftitle, "m.f");
1369 }
1370
1371 #[test]
1374 fn resolve_any_skips_aliased_entries_only_among_multiple_matches() {
1375 let mut data = PyDomainData::default();
1376 data.note_object("zeta.same", entry("index", "zeta.same", "function", false));
1377 data.note_object("beta.same", entry("index", "zeta.same", "function", true));
1378 data.note_object(
1379 "alpha.same",
1380 entry("index", "alpha.same", "function", false),
1381 );
1382 let results = resolve_any_xref(&data, None, None, "same");
1383 let names: Vec<&str> = results.iter().map(|(_, t)| t.reftitle.as_str()).collect();
1384 assert_eq!(
1385 names,
1386 vec!["zeta.same", "alpha.same"],
1387 "registration order, alias dropped"
1388 );
1389
1390 let mut lone = PyDomainData::default();
1391 lone.note_object("old.name", entry("index", "new_name", "function", true));
1392 let only = resolve_any_xref(&lone, None, None, "name");
1393 assert_eq!(only.len(), 1, "a single aliased match is kept");
1394 assert_eq!(only[0].1.reftitle, "old.name");
1395 }
1396
1397 #[test]
1401 fn resolve_any_module_candidates_carry_the_synopsis_reftitle() {
1402 let mut data = PyDomainData::default();
1403 data.note_object("syn", entry("index", "module-syn", "module", false));
1404 data.note_module(
1405 "syn",
1406 PyModuleEntry {
1407 docname: "index".to_string(),
1408 node_id: "module-syn".to_string(),
1409 synopsis: "The syn module.".to_string(),
1410 platform: String::new(),
1411 deprecated: false,
1412 },
1413 );
1414 let results = resolve_any_xref(&data, None, None, "syn");
1415 assert_eq!(results[0].1.reftitle, "syn: The syn module.");
1416 }
1417
1418 type ModindexRow<'a> = (&'a str, u8, &'a str, &'a str, &'a str, &'a str, &'a str);
1423
1424 fn modindex_rows(modindex: &PyModindex) -> Vec<(&str, Vec<ModindexRow<'_>>)> {
1426 modindex
1427 .groups
1428 .iter()
1429 .map(|group| {
1430 (
1431 group.letter.as_str(),
1432 group
1433 .entries
1434 .iter()
1435 .map(|e| {
1436 (
1437 e.name.as_str(),
1438 e.subtype,
1439 e.docname.as_str(),
1440 e.anchor.as_str(),
1441 e.extra.as_str(),
1442 e.qualifier.as_str(),
1443 e.descr.as_str(),
1444 )
1445 })
1446 .collect(),
1447 )
1448 })
1449 .collect()
1450 }
1451
1452 fn modindex_module(
1453 docname: &str,
1454 name: &str,
1455 synopsis: &str,
1456 platform: &str,
1457 deprecated: bool,
1458 ) -> PyModuleEntry {
1459 PyModuleEntry {
1460 docname: docname.to_string(),
1461 node_id: format!("module-{name}"),
1462 synopsis: synopsis.to_string(),
1463 platform: platform.to_string(),
1464 deprecated,
1465 }
1466 }
1467
1468 #[test]
1472 fn modindex_shapes_reproduces_the_probe_tuples() {
1473 let mut data = PyDomainData::default();
1474 for (name, synopsis, platform, deprecated) in [
1475 ("pkg", "", "", false),
1476 ("pkg.sub", "Sub synopsis.", "", false),
1477 ("pkg.sub2", "", "Windows", false),
1478 ("orphan.child", "", "", false),
1479 ("zzz", "", "", true),
1480 ] {
1481 data.note_module(
1482 name,
1483 modindex_module("index", name, synopsis, platform, deprecated),
1484 );
1485 }
1486 let modindex = generate_modindex(&data, &[]);
1487 assert!(!modindex.collapse);
1488 assert_eq!(
1489 modindex_rows(&modindex),
1490 vec![
1491 (
1492 "o",
1493 vec![
1494 ("orphan", 1, "", "", "", "", ""),
1495 (
1496 "orphan.child",
1497 2,
1498 "index",
1499 "module-orphan.child",
1500 "",
1501 "",
1502 ""
1503 ),
1504 ]
1505 ),
1506 (
1507 "p",
1508 vec![
1509 ("pkg", 1, "index", "module-pkg", "", "", ""),
1510 (
1511 "pkg.sub",
1512 2,
1513 "index",
1514 "module-pkg.sub",
1515 "",
1516 "",
1517 "Sub synopsis."
1518 ),
1519 ("pkg.sub2", 2, "index", "module-pkg.sub2", "Windows", "", ""),
1520 ]
1521 ),
1522 (
1523 "z",
1524 vec![("zzz", 0, "index", "module-zzz", "", "Deprecated", "")]
1525 ),
1526 ]
1527 );
1528 }
1529
1530 #[test]
1534 fn modindex_common_prefix_strips_for_bucketing_but_displays_full_names() {
1535 let mut data = PyDomainData::default();
1536 for name in ["pkg.aaa", "pkg.bbb", "other"] {
1537 data.note_module(name, modindex_module("index", name, "", "", false));
1538 }
1539 let modindex = generate_modindex(&data, &["pkg.".to_string()]);
1540 assert!(modindex.collapse);
1541 assert_eq!(
1542 modindex_rows(&modindex),
1543 vec![
1544 (
1545 "a",
1546 vec![("pkg.aaa", 0, "index", "module-pkg.aaa", "", "", "")]
1547 ),
1548 (
1549 "b",
1550 vec![("pkg.bbb", 0, "index", "module-pkg.bbb", "", "", "")]
1551 ),
1552 ("o", vec![("other", 0, "index", "module-other", "", "", "")]),
1553 ]
1554 );
1555 }
1556
1557 #[test]
1561 fn modindex_prefix_stripping_restores_emptied_names_and_prefers_longer() {
1562 let mut data = PyDomainData::default();
1563 for name in ["pkg", "pkgx", "pkg.deep.mod"] {
1564 data.note_module(name, modindex_module("index", name, "", "", false));
1565 }
1566 let modindex = generate_modindex(&data, &["pkg".to_string(), "pkg.deep.".to_string()]);
1567 assert_eq!(
1568 modindex_rows(&modindex),
1569 vec![
1570 (
1571 "m",
1572 vec![(
1573 "pkg.deep.mod",
1574 0,
1575 "index",
1576 "module-pkg.deep.mod",
1577 "",
1578 "",
1579 ""
1580 )]
1581 ),
1582 ("p", vec![("pkg", 0, "index", "module-pkg", "", "", "")]),
1583 ("x", vec![("pkgx", 0, "index", "module-pkgx", "", "", "")]),
1584 ],
1585 "pkg.deep.mod strips the longer prefix; pkg empties and restores \
1586 (bucketed under 'p', not dummy-parented); pkgx buckets under \
1587 its stripped 'x'"
1588 );
1589 assert!(modindex.collapse, "3 - 3 = 0 < 3");
1590 }
1591
1592 #[test]
1595 fn builtin_resolver_matches_sphinxs_exact_gates() {
1596 assert!(builtin_resolver("class", "None"));
1598 assert!(builtin_resolver("obj", "None"));
1599 assert!(
1600 !builtin_resolver("exc", "None"),
1601 "exc is not in the None gate"
1602 );
1603 assert!(builtin_resolver("class", "int"));
1605 assert!(builtin_resolver("obj", "bool"));
1606 assert!(builtin_resolver("exc", "ValueError"));
1607 assert!(builtin_resolver("class", "__loader__"), "getattr quirk");
1608 assert!(builtin_resolver("class", "Sequence"));
1610 assert!(builtin_resolver("class", "typing.Sequence"));
1611 assert!(builtin_resolver("obj", "Optional"));
1612 assert!(
1613 !builtin_resolver("class", "typing.typing.Sequence"),
1614 "removeprefix strips one prefix only"
1615 );
1616 assert!(!builtin_resolver("class", "Missing"));
1618 assert!(!builtin_resolver("func", "int"), "func is never silenced");
1619 assert!(
1620 !builtin_resolver("data", "int"),
1621 "probe: :py:data:`int` warns"
1622 );
1623 assert!(
1624 !builtin_resolver("exc", "len"),
1625 "a builtin function is not a class"
1626 );
1627 }
1628
1629 #[test]
1632 fn real_over_real_warns_and_the_last_definition_wins_in_place() {
1633 let mut py = PyDomainData::default();
1634 py.note_object("other", entry("a", "other", "function", false));
1635 assert_eq!(
1636 py.note_object("dup", entry("a", "dup", "function", false)),
1637 None
1638 );
1639 assert_eq!(
1640 py.note_object("dup", entry("b", "id0", "function", false)),
1641 Some("a".to_string()),
1642 "the second real definition warns naming the first's docname"
1643 );
1644 assert_eq!(
1645 object_rows(&py),
1646 vec![("other", "a", false), ("dup", "b", false)],
1647 "the overwrite lands in the original insertion slot"
1648 );
1649 assert_eq!(py.objects[py.objects_index["dup"]].1.node_id, "id0");
1650 assert_indices_consistent(&py);
1651 }
1652
1653 #[test]
1654 fn an_alias_never_replaces_a_real_definition_and_stays_silent() {
1655 let mut py = PyDomainData::default();
1656 py.note_object("name", entry("a", "name", "function", false));
1657 assert_eq!(
1658 py.note_object("name", entry("b", "alias-id", "function", true)),
1659 None
1660 );
1661 assert_eq!(
1662 py.objects[py.objects_index["name"]].1,
1663 entry("a", "name", "function", false),
1664 "the real entry is untouched"
1665 );
1666 }
1667
1668 #[test]
1669 fn a_real_definition_silently_overrides_an_alias_in_place() {
1670 let mut py = PyDomainData::default();
1671 py.note_object("first", entry("a", "first", "function", false));
1672 py.note_object("name", entry("a", "alias-id", "function", true));
1673 py.note_object("last", entry("a", "last", "function", false));
1674 assert_eq!(
1675 py.note_object("name", entry("b", "name", "function", false)),
1676 None,
1677 "\"The original definition found. Override it!\" — no warning"
1678 );
1679 assert_eq!(
1680 object_rows(&py),
1681 vec![
1682 ("first", "a", false),
1683 ("name", "b", false),
1684 ("last", "a", false)
1685 ],
1686 "the override keeps the alias's insertion slot"
1687 );
1688 }
1689
1690 #[test]
1697 fn an_alias_over_an_alias_warns_and_overwrites_in_place() {
1698 let mut py = PyDomainData::default();
1699 py.note_object("new_a", entry("index", "new_a", "function", false));
1700 py.note_object("shared.alias", entry("index", "new_a", "function", true));
1701 py.note_object("new_b", entry("index", "new_b", "function", false));
1702 assert_eq!(
1703 py.note_object("shared.alias", entry("index", "new_b", "function", true)),
1704 Some("index".to_string())
1705 );
1706 assert_eq!(
1707 object_rows(&py),
1708 vec![
1709 ("new_a", "index", false),
1710 ("shared.alias", "index", true),
1711 ("new_b", "index", false),
1712 ]
1713 );
1714 assert_eq!(
1715 py.objects[py.objects_index["shared.alias"]].1.node_id,
1716 "new_b"
1717 );
1718 }
1719
1720 #[test]
1726 fn iteration_preserves_registration_order_not_lexicographic_order() {
1727 let mut py = PyDomainData::default();
1728 py.note_object("zeta.same", entry("a", "zeta.same", "function", false));
1729 py.note_object("alpha.same", entry("a", "alpha.same", "function", false));
1730 assert_eq!(
1731 py.objects
1732 .iter()
1733 .map(|(n, _)| n.as_str())
1734 .collect::<Vec<_>>(),
1735 vec!["zeta.same", "alpha.same"]
1736 );
1737 assert_eq!(py.objects_index["zeta.same"], 0);
1738 assert_eq!(py.objects_index["alpha.same"], 1);
1739 }
1740
1741 #[test]
1742 fn clear_doc_preserves_the_relative_order_of_survivors() {
1743 let mut py = PyDomainData::default();
1744 py.note_object("one", entry("a", "one", "function", false));
1745 py.note_object("two", entry("b", "two", "function", false));
1746 py.note_object("three", entry("a", "three", "class", false));
1747 py.note_object("four", entry("b", "four", "function", false));
1748 py.note_module("amod", module_entry("a", "module-amod"));
1749 py.note_module("bmod", module_entry("b", "module-bmod"));
1750
1751 py.clear_doc("a");
1752
1753 assert_eq!(
1754 object_rows(&py),
1755 vec![("two", "b", false), ("four", "b", false)]
1756 );
1757 assert_eq!(
1758 py.modules
1759 .iter()
1760 .map(|(n, _)| n.as_str())
1761 .collect::<Vec<_>>(),
1762 vec!["bmod"]
1763 );
1764 assert_indices_consistent(&py);
1765
1766 py.clear_doc("b");
1767 assert!(py.objects.is_empty() && py.modules.is_empty());
1768 assert!(py.objects_index.is_empty() && py.modules_index.is_empty());
1769 }
1770
1771 #[test]
1772 fn merge_folds_only_the_named_docnames_in_registration_order() {
1773 let mut ours = PyDomainData::default();
1774 ours.note_object("kept", entry("a", "kept", "function", false));
1775 ours.note_object("both", entry("a", "both", "function", false));
1776
1777 let mut theirs = PyDomainData::default();
1778 theirs.note_object("zeta", entry("b", "zeta", "function", false));
1779 theirs.note_object("both", entry("b", "id0", "function", false));
1780 theirs.note_object("skipped", entry("c", "skipped", "function", false));
1781 theirs.note_module("bmod", module_entry("b", "module-bmod"));
1782 theirs.note_module("cmod", module_entry("c", "module-cmod"));
1783
1784 ours.merge(&theirs, &BTreeSet::from(["b".to_string()]));
1785
1786 assert_eq!(
1787 object_rows(&ours),
1788 vec![
1789 ("kept", "a", false),
1790 ("both", "b", false),
1794 ("zeta", "b", false),
1795 ]
1796 );
1797 assert_eq!(
1798 ours.modules
1799 .iter()
1800 .map(|(n, _)| n.as_str())
1801 .collect::<Vec<_>>(),
1802 vec!["bmod"]
1803 );
1804 assert_indices_consistent(&ours);
1805 }
1806
1807 #[test]
1808 fn note_module_never_warns_and_the_last_entry_wins_in_place() {
1809 let mut py = PyDomainData::default();
1810 py.note_module("mod", module_entry("a", "module-mod"));
1811 py.note_module("other", module_entry("a", "module-other"));
1812 py.note_module(
1813 "mod",
1814 PyModuleEntry {
1815 docname: "b".to_string(),
1816 node_id: "module-0".to_string(),
1817 synopsis: "S".to_string(),
1818 platform: "P".to_string(),
1819 deprecated: true,
1820 },
1821 );
1822 assert_eq!(
1823 py.modules
1824 .iter()
1825 .map(|(n, e)| (n.as_str(), e.docname.as_str()))
1826 .collect::<Vec<_>>(),
1827 vec![("mod", "b"), ("other", "a")]
1828 );
1829 assert!(py.modules[py.modules_index["mod"]].1.deprecated);
1830 }
1831
1832 fn parse(source: &str, docname: &str) -> crate::rst::ParseOutput {
1835 parse_rst_full(
1836 source,
1837 &ParseOptions {
1838 source_path: format!("<{docname}>"),
1839 sphinx: true,
1840 docname: docname.to_string(),
1841 found_docs: None,
1842 exclude_patterns: Vec::new(),
1843 py: Default::default(),
1844 srcdir: None,
1845 ..Default::default()
1846 },
1847 )
1848 }
1849
1850 fn read(sources: &[(&str, &str)]) -> (BuildEnvironment, Vec<BuildWarning>) {
1854 let mut env = BuildEnvironment::default();
1855 let mut warnings = Vec::new();
1856 let doc2path = |docname: &str| PathBuf::from(format!("/src/{docname}.rst"));
1857 for (docname, source) in sources {
1858 let parsed = parse(source, docname);
1859 let path = PathBuf::from(format!("/src/{docname}.rst"));
1860 std_domain::process_doc(
1861 &mut env,
1862 &DocumentSource {
1863 docname,
1864 doctree: &parsed.doctree,
1865 registry: &parsed.registry,
1866 path: &path,
1867 },
1868 &doc2path,
1869 &mut warnings,
1870 );
1871 }
1872 (env, warnings)
1873 }
1874
1875 #[test]
1880 fn a_py_object_defined_twice_in_one_document_warns_with_the_sphinx_bytes() {
1881 let (env, warnings) = read(&[(
1882 "index",
1883 ".. py:function:: dup()\n\n.. py:function:: dup()\n",
1884 )]);
1885 assert_eq!(
1886 warnings.iter().map(|w| w.render()).collect::<Vec<_>>(),
1887 vec![
1888 "<index>:3: WARNING: duplicate object description of dup, \
1889 other instance in index, use :no-index: for one of them"
1890 ]
1891 );
1892 assert_eq!(
1893 object_rows(&env.py),
1894 vec![("dup", "index", false)],
1895 "last definition wins"
1896 );
1897 assert_eq!(env.py.objects[0].1.node_id, "id0");
1898 }
1899
1900 #[test]
1905 fn a_module_defined_twice_warns_once_and_both_tables_keep_the_second() {
1906 let (env, warnings) =
1907 read(&[("index", ".. py:module:: dupmod\n\n.. py:module:: dupmod\n")]);
1908 assert_eq!(
1909 warnings.iter().map(|w| w.render()).collect::<Vec<_>>(),
1910 vec![
1911 "<index>:3: WARNING: duplicate object description of dupmod, \
1912 other instance in index, use :no-index: for one of them"
1913 ]
1914 );
1915 assert_eq!(
1916 env.py.objects[env.py.objects_index["dupmod"]].1,
1917 entry("index", "module-0", "module", false)
1918 );
1919 assert_eq!(
1920 env.py.modules[env.py.modules_index["dupmod"]].1,
1921 module_entry("index", "module-0")
1922 );
1923 }
1924
1925 #[test]
1929 fn a_py_duplicate_across_documents_names_the_other_docname() {
1930 let (env, warnings) = read(&[
1931 ("a", ".. py:function:: dup()\n"),
1932 ("b", "B\n=\n\n.. py:function:: dup()\n"),
1933 ]);
1934 assert_eq!(
1935 warnings.iter().map(|w| w.render()).collect::<Vec<_>>(),
1936 vec![
1937 "<b>:4: WARNING: duplicate object description of dup, \
1938 other instance in a, use :no-index: for one of them"
1939 ]
1940 );
1941 assert_eq!(object_rows(&env.py), vec![("dup", "b", false)]);
1942 }
1943
1944 #[test]
1948 fn canonical_registers_an_aliased_entry_with_the_same_node_id() {
1949 let (env, warnings) = read(&[(
1950 "index",
1951 ".. py:function:: new_name()\n :canonical: old.name\n",
1952 )]);
1953 assert!(warnings.is_empty(), "{warnings:?}");
1954 assert_eq!(
1955 env.py.objects,
1956 vec![
1957 (
1958 "new_name".to_string(),
1959 entry("index", "new_name", "function", false)
1960 ),
1961 (
1962 "old.name".to_string(),
1963 entry("index", "new_name", "function", true)
1964 ),
1965 ]
1966 );
1967 }
1968
1969 #[test]
1975 fn py_duplicate_warnings_interleave_with_std_s_in_document_order() {
1976 let document = "Probe\n=====\n\n\
1977 .. envvar:: STDDUP\n\n\
1978 .. py:function:: pydup()\n\n\
1979 .. envvar:: STDDUP\n\n\
1980 .. glossary::\n\n \
1981 gterm\n First.\n\n\
1982 .. py:function:: pydup()\n\n\
1983 .. glossary::\n\n \
1984 gterm\n Second.\n";
1985 let (_, warnings) = read(&[("index", document)]);
1986 assert_eq!(
1987 warnings
1988 .iter()
1989 .map(|warning| (warning.line, warning.message.as_str()))
1990 .collect::<Vec<_>>(),
1991 vec![
1992 (
1993 Some(8),
1994 "duplicate envvar description of STDDUP, other instance in index"
1995 ),
1996 (
1997 Some(15),
1998 "duplicate object description of pydup, other instance in index, \
1999 use :no-index: for one of them"
2000 ),
2001 (
2002 Some(18),
2003 "duplicate term description of gterm, other instance in index"
2004 ),
2005 ],
2006 "{warnings:?}"
2007 );
2008 }
2009
2010 #[test]
2015 fn py_and_std_registrations_stay_in_their_own_registries() {
2016 let (env, warnings) = read(&[("index", ".. py:function:: func()\n\n.. envvar:: HOME\n")]);
2017 assert!(warnings.is_empty(), "{warnings:?}");
2018 assert_eq!(object_rows(&env.py), vec![("func", "index", false)]);
2019 assert_eq!(
2020 env.std.objects.keys().collect::<Vec<_>>(),
2021 vec![&("envvar".to_string(), "HOME".to_string())]
2022 );
2023 assert!(env
2024 .std
2025 .objects
2026 .keys()
2027 .all(|(objtype, _)| objtype != "function"));
2028 }
2029}