1use std::collections::HashSet;
81use std::fmt;
82
83use crate::doctree::{kinds, AttrValue, Node, Span};
84
85use super::annotations::{
86 desc_sig_operator, desc_sig_punctuation, desc_sig_space, parse_annotation, PyRefContext,
87};
88use super::expr::{
89 self, parse_py_expr, parse_py_star_annotation, PyBoolOp, PyConst, PyExpr, PyOp, PyUnaryOp,
90};
91use super::PySigConfig;
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum ParamKind {
97 PositionalOnly,
98 PositionalOrKeyword,
99 VarPositional,
100 KeywordOnly,
101 VarKeyword,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct PyParam {
110 pub name: String,
111 pub kind: ParamKind,
112 pub annotation: Option<String>,
113 pub default: Option<String>,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
129pub enum SigParseError {
130 Syntax(String),
133 Duplicate(String),
135}
136
137impl fmt::Display for SigParseError {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 match self {
140 SigParseError::Syntax(msg) => f.write_str(msg),
141 SigParseError::Duplicate(name) => write!(f, "duplicate parameter name: '{name}'"),
143 }
144 }
145}
146
147impl std::error::Error for SigParseError {}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct PySigMatch {
158 pub prefix: Option<String>,
160 pub name: String,
162 pub tp_list: Option<String>,
164 pub arglist: Option<String>,
166 pub retann: Option<String>,
168 pub tp_span: (usize, usize),
170 pub arg_span: (usize, usize),
172}
173
174#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
177pub struct SingleLineOpts {
178 pub parameter_list: bool,
180 pub type_parameter_list: bool,
182}
183
184pub fn multi_line_flags(
191 sig: &str,
192 m: &PySigMatch,
193 opts: SingleLineOpts,
194 cfg: &PySigConfig,
195) -> (bool, bool) {
196 let max_len = cfg.max_len();
197 let sig_len = sig.chars().count() as i64;
198 let width = |span: (usize, usize)| -> i64 {
199 sig.get(span.0..span.1)
200 .map(|s| s.chars().count() as i64)
201 .unwrap_or(0)
202 };
203 let parameter_list =
204 !opts.parameter_list && (sig_len - width(m.tp_span)) > max_len && max_len > 0;
205 let type_parameter_list =
206 !opts.type_parameter_list && (sig_len - width(m.arg_span)) > max_len && max_len > 0;
207 (parameter_list, type_parameter_list)
208}
209
210pub fn signature_from_str(arglist: &str) -> Result<Vec<PyParam>, SigParseError> {
219 if arglist.trim().is_empty() {
220 return Ok(Vec::new());
221 }
222 let toks = lex(arglist).ok_or_else(invalid_syntax)?;
223 let items = split_top_level(&toks)?;
224
225 let mut params: Vec<PyParam> = Vec::new();
226 let mut seen_slash = false;
227 let mut seen_star = false;
228 let mut bare_star = false;
229 let mut seen_kwargs = false;
230 let mut kwonly_count = 0usize;
231 let mut pos_default_seen = false;
232
233 let last = items.len() - 1;
234 for (i, item) in items.iter().enumerate() {
235 let shape = classify_item(item);
236 if matches!(shape, ItemShape::Empty) {
237 if i == last && i > 0 {
240 continue;
241 }
242 return Err(invalid_syntax());
243 }
244 if seen_kwargs {
245 return Err(SigParseError::Syntax(
246 "arguments cannot follow var-keyword argument".to_string(),
247 ));
248 }
249 match shape {
250 ItemShape::Empty => unreachable!("handled above"),
251 ItemShape::Slash => {
252 if seen_star {
253 return Err(SigParseError::Syntax("/ must be ahead of *".to_string()));
254 }
255 if seen_slash {
256 return Err(SigParseError::Syntax("/ may appear only once".to_string()));
257 }
258 if params.is_empty() {
259 return Err(SigParseError::Syntax(
260 "at least one argument must precede /".to_string(),
261 ));
262 }
263 seen_slash = true;
264 for p in &mut params {
265 p.kind = ParamKind::PositionalOnly;
266 }
267 }
268 ItemShape::BareStar => {
269 if seen_star {
270 return Err(SigParseError::Syntax(
271 "* argument may appear only once".to_string(),
272 ));
273 }
274 seen_star = true;
275 bare_star = true;
276 }
277 ItemShape::VarArgs(rest) => {
278 if seen_star {
279 return Err(SigParseError::Syntax(
280 "* argument may appear only once".to_string(),
281 ));
282 }
283 seen_star = true;
284 let raw = parse_param_tokens(rest, arglist, true)?;
285 if raw.default.is_some() {
286 return Err(SigParseError::Syntax(
287 "var-positional argument cannot have default value".to_string(),
288 ));
289 }
290 params.push(PyParam {
291 name: raw.name,
292 kind: ParamKind::VarPositional,
293 annotation: raw.annotation,
294 default: None,
295 });
296 }
297 ItemShape::KwArgs(rest) => {
298 let raw = parse_param_tokens(rest, arglist, false)?;
299 if raw.default.is_some() {
300 return Err(SigParseError::Syntax(
301 "var-keyword argument cannot have default value".to_string(),
302 ));
303 }
304 seen_kwargs = true;
305 params.push(PyParam {
306 name: raw.name,
307 kind: ParamKind::VarKeyword,
308 annotation: raw.annotation,
309 default: None,
310 });
311 }
312 ItemShape::Plain(toks) => {
313 let raw = parse_param_tokens(toks, arglist, false)?;
314 let kind = if seen_star {
315 kwonly_count += 1;
316 ParamKind::KeywordOnly
317 } else {
318 if raw.default.is_none() && pos_default_seen {
322 return Err(SigParseError::Syntax(
323 "parameter without a default follows parameter with a default"
324 .to_string(),
325 ));
326 }
327 if raw.default.is_some() {
328 pos_default_seen = true;
329 }
330 ParamKind::PositionalOrKeyword
331 };
332 params.push(PyParam {
333 name: raw.name,
334 kind,
335 annotation: raw.annotation,
336 default: raw.default,
337 });
338 }
339 }
340 }
341 if bare_star && kwonly_count == 0 {
342 return Err(SigParseError::Syntax(
343 "named arguments must follow bare *".to_string(),
344 ));
345 }
346
347 let mut seen: HashSet<&str> = HashSet::new();
349 for p in ¶ms {
350 if !seen.insert(p.name.as_str()) {
351 return Err(SigParseError::Duplicate(p.name.clone()));
352 }
353 }
354 Ok(params)
355}
356
357pub fn parse_arglist(
364 arglist: &str,
365 multi_line: bool,
366 ctx: &PyRefContext,
367 cfg: &PySigConfig,
368) -> Result<Node, SigParseError> {
369 let sig_params = signature_from_str(arglist)?;
370 let mut params = attr_list_node("desc_parameterlist", multi_line, cfg);
371 let mut last_kind: Option<ParamKind> = None;
372 for param in &sig_params {
373 if param.kind != ParamKind::PositionalOnly && last_kind == Some(ParamKind::PositionalOnly) {
374 params.children.push(positional_only_separator());
375 }
376 if param.kind == ParamKind::KeywordOnly
377 && matches!(
378 last_kind,
379 Some(ParamKind::PositionalOrKeyword) | Some(ParamKind::PositionalOnly) | None
380 )
381 {
382 params.children.push(keyword_only_separator());
383 }
384
385 let mut node = desc_parameter();
386 match param.kind {
387 ParamKind::VarPositional => {
388 node.children.push(desc_sig_operator("*"));
389 node.children.push(desc_sig_name_text(¶m.name));
390 }
391 ParamKind::VarKeyword => {
392 node.children.push(desc_sig_operator("**"));
393 node.children.push(desc_sig_name_text(¶m.name));
394 }
395 _ => node.children.push(desc_sig_name_text(¶m.name)),
396 }
397 if let Some(ann) = ¶m.annotation {
398 node.children.push(desc_sig_punctuation(":"));
399 node.children.push(desc_sig_space());
400 node.children
401 .push(annotation_wrapper(parse_annotation(ann, ctx, cfg)));
402 }
403 if let Some(default) = ¶m.default {
404 if param.annotation.is_some() {
405 node.children.push(desc_sig_space());
406 node.children.push(desc_sig_operator("="));
407 node.children.push(desc_sig_space());
408 } else {
409 node.children.push(desc_sig_operator("="));
410 }
411 node.children.push(default_value_inline(default));
412 }
413 params.children.push(node);
414 last_kind = Some(param.kind);
415 }
416 if last_kind == Some(ParamKind::PositionalOnly) {
418 params.children.push(positional_only_separator());
419 }
420 Ok(params)
421}
422
423pub fn pseudo_parse_arglist(
436 arglist: &str,
437 multi_line: bool,
438 ctx: &PyRefContext,
439 cfg: &PySigConfig,
440) -> Node {
441 let list = attr_list_node("desc_parameterlist", multi_line, cfg);
442 match pseudo_build(arglist, list, ctx, cfg) {
443 Some(done) => done,
444 None => {
445 let mut fresh = Node::elem("desc_parameterlist", Span::ZERO);
448 fresh.set("xml:space", AttrValue::Str("preserve".to_string()));
449 let mut par = desc_parameter();
450 if !arglist.is_empty() {
451 par.children.push(Node::text_node(arglist, Span::ZERO));
452 }
453 fresh.children.push(par);
454 fresh
455 }
456 }
457}
458
459pub fn parse_type_list(
466 tp_list: &str,
467 multi_line: bool,
468 ctx: &PyRefContext,
469 cfg: &PySigConfig,
470) -> Result<Node, SigParseError> {
471 let cleaned = tp_list.replace('\n', "");
473 let cleaned = cleaned.trim();
474 let toks = lex(cleaned).ok_or_else(invalid_syntax)?;
475 let mut level = 0i64;
483 for t in &toks {
484 if t.kind == TokKind::Op {
485 match t.text.as_str() {
486 "(" | "[" | "{" => level += 1,
487 ")" | "]" | "}" => level -= 1,
488 _ => {}
489 }
490 }
491 }
492 if level > 0 {
493 return Err(SigParseError::Syntax(
494 "('unexpected EOF in multi-line statement', (1, 0))".to_string(),
495 ));
496 }
497
498 let type_params = tp_parse(&toks)?;
499
500 let mut list = attr_list_node("desc_type_parameter_list", multi_line, cfg);
501 for tp in &type_params {
502 let mut node = Node::elem("desc_type_parameter", Span::ZERO);
503 node.set("xml:space", AttrValue::Str("preserve".to_string()));
504 match tp.kind {
505 ParamKind::VarPositional => node.children.push(desc_sig_operator("*")),
506 ParamKind::VarKeyword => node.children.push(desc_sig_operator("**")),
507 _ => {}
508 }
509 node.children.push(desc_sig_name_text(&tp.name));
510
511 if let Some(ann_text) = &tp.annotation {
512 let children = parse_annotation(ann_text, ctx, cfg);
513 if children.is_empty() {
514 continue;
517 }
518 node.children.push(desc_sig_punctuation(":"));
519 node.children.push(desc_sig_space());
520 let wrapper = annotation_wrapper(children);
521 if ann_text.starts_with('(') && ann_text.ends_with(')') {
525 let text = wrapper.astext();
526 if text.starts_with('(') && text.ends_with(')') {
527 node.children.push(wrapper);
528 } else {
529 node.children.push(desc_sig_punctuation("("));
530 node.children.push(wrapper);
531 node.children.push(desc_sig_punctuation(")"));
532 }
533 } else {
534 node.children.push(wrapper);
535 }
536 }
537 if let Some(default) = &tp.default {
538 node.children.push(desc_sig_space());
541 node.children.push(desc_sig_operator("="));
542 node.children.push(desc_sig_space());
543 node.children.push(default_value_inline(default));
544 }
545 list.children.push(node);
546 }
547 Ok(list)
548}
549
550fn attr_list_node(kind: &'static str, multi_line: bool, cfg: &PySigConfig) -> Node {
558 let mut node = Node::elem(kind, Span::ZERO);
559 node.set(
560 "multi_line_parameter_list",
561 AttrValue::Int(i64::from(multi_line)),
562 );
563 node.set(
564 "multi_line_trailing_comma",
565 AttrValue::Int(i64::from(
566 cfg.python_trailing_comma_in_multi_line_signatures,
567 )),
568 );
569 node.set("xml:space", AttrValue::Str("preserve".to_string()));
570 node
571}
572
573fn desc_parameter() -> Node {
574 let mut node = Node::elem("desc_parameter", Span::ZERO);
575 node.set("xml:space", AttrValue::Str("preserve".to_string()));
576 node
577}
578
579fn desc_optional() -> Node {
580 let mut node = Node::elem("desc_optional", Span::ZERO);
581 node.set("xml:space", AttrValue::Str("preserve".to_string()));
582 node
583}
584
585fn desc_sig_name_text(text: &str) -> Node {
589 if text.is_empty() {
590 let mut node = Node::elem("desc_sig_name", Span::ZERO);
591 node.attrs.classes.push("n".to_string());
592 node
593 } else {
594 super::annotations::desc_sig_name(text)
595 }
596}
597
598fn annotation_wrapper(children: Vec<Node>) -> Node {
601 let mut node = Node::elem("desc_sig_name", Span::ZERO);
602 node.attrs.classes.push("n".to_string());
603 node.children = children;
604 node
605}
606
607fn default_value_inline(text: &str) -> Node {
610 let mut node = Node::elem("inline", Span::ZERO);
611 node.attrs.classes.push("default_value".to_string());
612 node.set("support_smartquotes", AttrValue::Int(0));
613 if !text.is_empty() {
614 node.children.push(Node::text_node(text, Span::ZERO));
615 }
616 node
617}
618
619fn separator(op_text: &str, class: &str, explanation: &str) -> Node {
623 let mut abbr = Node::elem(kinds::ABBREVIATION, Span::ZERO);
624 abbr.set("explanation", AttrValue::Str(explanation.to_string()));
625 abbr.children.push(Node::text_node(op_text, Span::ZERO));
626 let mut op = Node::elem("desc_sig_operator", Span::ZERO);
627 op.attrs.classes.push(class.to_string());
628 op.attrs.classes.push("o".to_string());
629 op.children.push(abbr);
630 let mut par = desc_parameter();
631 par.children.push(op);
632 par
633}
634
635fn positional_only_separator() -> Node {
636 separator(
637 "/",
638 "positional-only-separator",
639 "Positional-only parameter separator (PEP 570)",
640 )
641}
642
643fn keyword_only_separator() -> Node {
644 separator(
645 "*",
646 "keyword-only-separator",
647 "Keyword-only parameters separator (PEP 3102)",
648 )
649}
650
651fn pseudo_build(arglist: &str, list: Node, ctx: &PyRefContext, cfg: &PySigConfig) -> Option<Node> {
659 let mut stack: Vec<Node> = vec![list];
660 for argument in arglist.split(',') {
661 let mut argument = argument.trim();
662 let mut ends_open = 0usize;
663 let mut ends_close = 0usize;
664 while let Some(rest) = argument.strip_prefix('[') {
665 stack_push(&mut stack)?;
666 argument = rest.trim();
667 }
668 while let Some(rest) = argument.strip_prefix(']') {
669 stack_pop(&mut stack)?;
670 argument = rest.trim();
671 }
672 while argument.ends_with(']') && !argument.ends_with("[]") {
673 ends_close += 1;
674 argument = argument[..argument.len() - 1].trim();
675 }
676 while let Some(rest) = argument.strip_suffix('[') {
677 ends_open += 1;
678 argument = rest.trim();
679 }
680 if !argument.is_empty() {
681 let (param_with_annotation, default_value) = match argument.split_once('=') {
684 Some((head, tail)) => (head, tail),
685 None => (argument, ""),
686 };
687 let (param_name, annotation) = match param_with_annotation.split_once(':') {
688 Some((head, tail)) => (head, tail),
689 None => (param_with_annotation, ""),
690 };
691 let mut node = desc_parameter();
692 node.children.push(desc_sig_name_text(param_name.trim()));
693 if !annotation.is_empty() {
694 node.children.push(desc_sig_punctuation(":"));
695 node.children.push(desc_sig_space());
696 node.children.push(annotation_wrapper(parse_annotation(
697 annotation.trim(),
698 ctx,
699 cfg,
700 )));
701 }
702 if !default_value.is_empty() {
703 if !annotation.is_empty() {
704 node.children.push(desc_sig_space());
705 }
706 node.children.push(desc_sig_operator("="));
707 if !annotation.is_empty() {
708 node.children.push(desc_sig_space());
709 }
710 node.children
711 .push(default_value_inline(default_value.trim()));
712 }
713 stack.last_mut()?.children.push(node);
714 }
715 for _ in 0..ends_open {
716 stack_push(&mut stack)?;
717 }
718 for _ in 0..ends_close {
719 stack_pop(&mut stack)?;
720 }
721 }
722 if stack.len() == 1 {
723 stack.pop()
724 } else {
725 None
726 }
727}
728
729fn stack_push(stack: &mut Vec<Node>) -> Option<()> {
732 if stack.is_empty() {
733 return None;
734 }
735 stack.push(desc_optional());
736 Some(())
737}
738
739fn stack_pop(stack: &mut Vec<Node>) -> Option<()> {
744 let top = stack.pop()?;
745 if let Some(parent) = stack.last_mut() {
746 parent.children.push(top);
747 }
748 Some(())
749}
750
751#[derive(Debug, Clone, Copy, PartialEq, Eq)]
757enum TokKind {
758 Name,
759 Number,
760 Str,
761 Op,
762}
763
764#[derive(Debug, Clone)]
765struct Tok {
766 kind: TokKind,
767 text: String,
768 start: usize,
770 end: usize,
771}
772
773fn is_name_start(c: char) -> bool {
774 c.is_alphabetic() || c == '_'
775}
776
777fn is_name_continue(c: char) -> bool {
778 c.is_alphanumeric() || c == '_'
779}
780
781fn is_string_prefix(word: &str) -> bool {
783 !word.is_empty() && word.len() <= 2 && word.chars().all(|c| "rRbBuUfF".contains(c))
784}
785
786const OPS3: &[&str] = &["**=", "//=", "<<=", ">>=", "..."];
787const OPS2: &[&str] = &[
788 "**", "//", "<<", ">>", "<=", ">=", "==", "!=", "->", ":=", "+=", "-=", "*=", "/=", "%=", "@=",
789 "&=", "|=", "^=",
790];
791
792fn lex(src: &str) -> Option<Vec<Tok>> {
799 let mut toks: Vec<Tok> = Vec::new();
800 let mut iter = src.char_indices().peekable();
801 while let Some(&(i, c)) = iter.peek() {
802 if c.is_whitespace() {
803 iter.next();
804 continue;
805 }
806 if is_name_start(c) {
807 let mut end = i + c.len_utf8();
808 iter.next();
809 while let Some(&(j, d)) = iter.peek() {
810 if is_name_continue(d) {
811 end = j + d.len_utf8();
812 iter.next();
813 } else {
814 break;
815 }
816 }
817 let word = &src[i..end];
818 if let Some(&(_, q)) = iter.peek() {
819 if (q == '\'' || q == '"') && is_string_prefix(word) {
820 let send = lex_string(&mut iter)?;
821 toks.push(Tok {
822 kind: TokKind::Str,
823 text: src[i..send].to_string(),
824 start: i,
825 end: send,
826 });
827 continue;
828 }
829 }
830 toks.push(Tok {
831 kind: TokKind::Name,
832 text: word.to_string(),
833 start: i,
834 end,
835 });
836 continue;
837 }
838 if c.is_ascii_digit() || (c == '.' && peek2_is_digit(&iter)) {
839 let end = lex_number(src, &mut iter);
840 toks.push(Tok {
841 kind: TokKind::Number,
842 text: src[i..end].to_string(),
843 start: i,
844 end,
845 });
846 continue;
847 }
848 if c == '\'' || c == '"' {
849 let send = lex_string(&mut iter)?;
850 toks.push(Tok {
851 kind: TokKind::Str,
852 text: src[i..send].to_string(),
853 start: i,
854 end: send,
855 });
856 continue;
857 }
858 let rest = &src[i..];
860 let mut matched = None;
861 for cand in OPS3.iter().chain(OPS2.iter()) {
862 if rest.starts_with(*cand) {
863 matched = Some(*cand);
864 break;
865 }
866 }
867 let op_len = matched.map_or(c.len_utf8(), str::len);
868 let end = i + op_len;
869 toks.push(Tok {
870 kind: TokKind::Op,
871 text: src[i..end].to_string(),
872 start: i,
873 end,
874 });
875 for _ in 0..src[i..end].chars().count() {
876 iter.next();
877 }
878 }
879 Some(toks)
880}
881
882fn peek2_is_digit(iter: &std::iter::Peekable<std::str::CharIndices<'_>>) -> bool {
883 let mut it = iter.clone();
884 it.next();
885 matches!(it.peek(), Some(&(_, d)) if d.is_ascii_digit())
886}
887
888fn lex_number(src: &str, iter: &mut std::iter::Peekable<std::str::CharIndices<'_>>) -> usize {
894 let (start, first) = *iter.peek().expect("caller peeked a digit");
895 let radix_prefixed = {
896 let rest = &src[start..];
897 rest.len() >= 2
898 && rest.starts_with('0')
899 && matches!(
900 rest[1..].chars().next(),
901 Some('x' | 'X' | 'b' | 'B' | 'o' | 'O')
902 )
903 };
904 let mut end = start + first.len_utf8();
905 let mut prev = first;
906 iter.next();
907 while let Some(&(j, d)) = iter.peek() {
908 let continues = d.is_ascii_alphanumeric()
909 || d == '_'
910 || d == '.'
911 || ((d == '+' || d == '-') && matches!(prev, 'e' | 'E') && !radix_prefixed);
912 if continues {
913 end = j + d.len_utf8();
914 prev = d;
915 iter.next();
916 } else {
917 break;
918 }
919 }
920 end
921}
922
923fn lex_string(iter: &mut std::iter::Peekable<std::str::CharIndices<'_>>) -> Option<usize> {
928 let (_, quote) = iter.next()?;
929 let mut probe = iter.clone();
931 let triple = matches!(
932 (probe.next(), probe.next()),
933 (Some((_, a)), Some((_, b))) if a == quote && b == quote
934 );
935 if triple {
936 iter.next();
937 iter.next();
938 loop {
939 let (_, c) = iter.next()?;
940 if c == '\\' {
941 iter.next();
942 continue;
943 }
944 if c == quote {
945 let mut probe = iter.clone();
946 if matches!(
947 (probe.next(), probe.next()),
948 (Some((_, a)), Some((_, b))) if a == quote && b == quote
949 ) {
950 iter.next();
951 let (j, q) = iter.next().expect("probed above");
952 return Some(j + q.len_utf8());
953 }
954 }
955 }
956 } else {
957 loop {
958 let (j, c) = iter.next()?;
959 if c == '\\' {
960 iter.next();
961 continue;
962 }
963 if c == quote {
964 return Some(j + c.len_utf8());
965 }
966 if c == '\n' {
967 return None;
968 }
969 }
970 }
971}
972
973fn invalid_syntax() -> SigParseError {
978 SigParseError::Syntax("invalid syntax".to_string())
979}
980
981fn split_top_level(toks: &[Tok]) -> Result<Vec<&[Tok]>, SigParseError> {
986 let mut items: Vec<&[Tok]> = Vec::new();
987 let mut depth = 0i64;
988 let mut start = 0usize;
989 for (idx, t) in toks.iter().enumerate() {
990 match t.kind {
991 TokKind::Op => match t.text.as_str() {
992 "(" | "[" | "{" => depth += 1,
993 ")" | "]" | "}" => {
994 depth -= 1;
995 if depth < 0 {
996 return Err(invalid_syntax());
997 }
998 }
999 "," if depth == 0 => {
1000 items.push(&toks[start..idx]);
1001 start = idx + 1;
1002 }
1003 _ => {}
1004 },
1005 TokKind::Name if depth == 0 && t.text == "lambda" => {
1006 return Err(invalid_syntax());
1007 }
1008 _ => {}
1009 }
1010 }
1011 if depth != 0 {
1012 return Err(invalid_syntax());
1013 }
1014 items.push(&toks[start..]);
1015 Ok(items)
1016}
1017
1018enum ItemShape<'t> {
1019 Empty,
1020 Slash,
1021 BareStar,
1022 VarArgs(&'t [Tok]),
1023 KwArgs(&'t [Tok]),
1024 Plain(&'t [Tok]),
1025}
1026
1027fn classify_item<'t>(toks: &'t [Tok]) -> ItemShape<'t> {
1028 let Some(first) = toks.first() else {
1029 return ItemShape::Empty;
1030 };
1031 if first.kind == TokKind::Op {
1032 match first.text.as_str() {
1033 "/" if toks.len() == 1 => return ItemShape::Slash,
1034 "*" if toks.len() == 1 => return ItemShape::BareStar,
1035 "**" => return ItemShape::KwArgs(&toks[1..]),
1036 "*" => return ItemShape::VarArgs(&toks[1..]),
1037 _ => {}
1038 }
1039 }
1040 ItemShape::Plain(toks)
1041}
1042
1043struct RawParam {
1044 name: String,
1045 annotation: Option<String>,
1046 default: Option<String>,
1047}
1048
1049fn parse_param_tokens(
1059 toks: &[Tok],
1060 src: &str,
1061 star_annotation: bool,
1062) -> Result<RawParam, SigParseError> {
1063 if toks.is_empty() {
1064 return Err(invalid_syntax());
1065 }
1066 let mut depth = 0i64;
1067 let mut colon: Option<usize> = None;
1068 let mut eq: Option<usize> = None;
1069 for (i, t) in toks.iter().enumerate() {
1070 if t.kind == TokKind::Op {
1071 match t.text.as_str() {
1072 "(" | "[" | "{" => depth += 1,
1073 ")" | "]" | "}" => depth -= 1,
1074 ":" if depth == 0 && colon.is_none() && eq.is_none() => colon = Some(i),
1075 "=" if depth == 0 && eq.is_none() => eq = Some(i),
1076 _ => {}
1077 }
1078 }
1079 }
1080 let name_end = colon.or(eq).unwrap_or(toks.len());
1081 let name_toks = &toks[..name_end];
1082 if name_toks.len() != 1 || name_toks[0].kind != TokKind::Name {
1083 return Err(invalid_syntax());
1084 }
1085 let name = match parse_py_expr(&name_toks[0].text) {
1086 Ok(PyExpr::Name(n)) => n,
1087 _ => return Err(invalid_syntax()),
1088 };
1089 let ann_end = eq.unwrap_or(toks.len());
1090 let annotation = match colon {
1091 Some(ci) => Some(render_fragment(
1092 &toks[ci + 1..ann_end],
1093 src,
1094 star_annotation,
1095 )?),
1096 None => None,
1097 };
1098 let default = match eq {
1099 Some(ei) => Some(render_fragment(&toks[ei + 1..], src, false)?),
1100 None => None,
1101 };
1102 Ok(RawParam {
1103 name,
1104 annotation,
1105 default,
1106 })
1107}
1108
1109fn render_fragment(
1110 toks: &[Tok],
1111 src: &str,
1112 star_annotation: bool,
1113) -> Result<String, SigParseError> {
1114 if toks.is_empty() {
1115 return Err(invalid_syntax());
1116 }
1117 let fragment = &src[toks[0].start..toks[toks.len() - 1].end];
1118 pycode_unparse(fragment, star_annotation)
1119}
1120
1121enum RenderErr {
1126 SourceMismatch,
1129 Unrenderable,
1132}
1133
1134struct NumPool {
1135 texts: Vec<String>,
1136 idx: usize,
1137}
1138
1139fn pycode_unparse(fragment: &str, star_annotation: bool) -> Result<String, SigParseError> {
1146 let parsed = if star_annotation {
1147 parse_py_star_annotation(fragment)
1148 } else {
1149 parse_py_expr(fragment)
1150 }
1151 .map_err(|_| invalid_syntax())?;
1152 let texts: Vec<String> = lex(fragment)
1153 .map(|toks| {
1154 toks.into_iter()
1155 .filter(|t| t.kind == TokKind::Number)
1156 .map(|t| t.text)
1157 .collect()
1158 })
1159 .unwrap_or_default();
1160 let mut pool = Some(NumPool { texts, idx: 0 });
1161 match render_pycode(&parsed, &mut pool) {
1162 Ok(s) if pool.as_ref().is_some_and(|p| p.idx == p.texts.len()) => Ok(s),
1163 Ok(_) | Err(RenderErr::SourceMismatch) => {
1164 let mut no_pool = None;
1165 render_pycode(&parsed, &mut no_pool).map_err(|_| invalid_syntax())
1166 }
1167 Err(RenderErr::Unrenderable) => Err(invalid_syntax()),
1168 }
1169}
1170
1171fn binop_text(op: PyOp) -> &'static str {
1172 match op {
1173 PyOp::Add => "+",
1174 PyOp::Sub => "-",
1175 PyOp::Mult => "*",
1176 PyOp::MatMult => "@",
1177 PyOp::Div => "/",
1178 PyOp::Mod => "%",
1179 PyOp::Pow => "**",
1180 PyOp::LShift => "<<",
1181 PyOp::RShift => ">>",
1182 PyOp::BitOr => "|",
1183 PyOp::BitXor => "^",
1184 PyOp::BitAnd => "&",
1185 PyOp::FloorDiv => "//",
1186 }
1187}
1188
1189fn render_join(elts: &[PyExpr], pool: &mut Option<NumPool>) -> Result<String, RenderErr> {
1190 let mut parts = Vec::with_capacity(elts.len());
1191 for e in elts {
1192 parts.push(render_pycode(e, pool)?);
1193 }
1194 Ok(parts.join(", "))
1195}
1196
1197fn render_pycode(e: &PyExpr, pool: &mut Option<NumPool>) -> Result<String, RenderErr> {
1198 Ok(match e {
1199 PyExpr::Name(id) => id.clone(),
1200 PyExpr::Attribute(value, attr) => format!("{}.{attr}", render_pycode(value, pool)?),
1201 PyExpr::BinOp { left, op, right } => {
1202 let l = render_pycode(left, pool)?;
1203 let r = render_pycode(right, pool)?;
1204 let o = binop_text(*op);
1205 if matches!(op, PyOp::Pow) {
1207 format!("{l}{o}{r}")
1208 } else {
1209 format!("{l} {o} {r}")
1210 }
1211 }
1212 PyExpr::UnaryOp { op, operand } => {
1213 let s = render_pycode(operand, pool)?;
1214 match op {
1215 PyUnaryOp::Not => format!("not {s}"),
1216 PyUnaryOp::Invert => format!("~{s}"),
1217 PyUnaryOp::UAdd => format!("+{s}"),
1218 PyUnaryOp::USub => format!("-{s}"),
1219 }
1220 }
1221 PyExpr::Constant(c) => const_text(c, pool)?,
1222 PyExpr::Tuple(elts) => match elts.len() {
1223 0 => "()".to_string(),
1224 1 => format!("({},)", render_pycode(&elts[0], pool)?),
1225 _ => format!("({})", render_join(elts, pool)?),
1226 },
1227 PyExpr::List(elts) => format!("[{}]", render_join(elts, pool)?),
1228 PyExpr::Set(elts) => format!("{{{}}}", render_join(elts, pool)?),
1229 PyExpr::Dict(entries) => {
1230 let mut parts = Vec::with_capacity(entries.len());
1231 for (key, value) in entries {
1232 let Some(key) = key else {
1235 return Err(RenderErr::Unrenderable);
1236 };
1237 parts.push(format!(
1238 "{}: {}",
1239 render_pycode(key, pool)?,
1240 render_pycode(value, pool)?
1241 ));
1242 }
1243 format!("{{{}}}", parts.join(", "))
1244 }
1245 PyExpr::Call { func, args, kwargs } => {
1246 let f = render_pycode(func, pool)?;
1253 let mut parts: Vec<String> = Vec::with_capacity(args.len() + kwargs.len());
1254 for a in args {
1255 parts.push(render_pycode(a, pool)?);
1256 }
1257 for (k, v) in kwargs {
1258 parts.push(format!("{k}={}", render_pycode(v, pool)?));
1259 }
1260 format!("{f}({})", parts.join(", "))
1261 }
1262 PyExpr::BoolOp { op, values } => {
1266 let sep = match op {
1267 PyBoolOp::And => " and ",
1268 PyBoolOp::Or => " or ",
1269 };
1270 let mut parts = Vec::with_capacity(values.len());
1271 for v in values {
1272 parts.push(render_pycode(v, pool)?);
1273 }
1274 parts.join(sep)
1275 }
1276 PyExpr::Starred(value) => format!("*{}", render_pycode(value, pool)?),
1277 PyExpr::Subscript { value, slice } => {
1278 let v = render_pycode(value, pool)?;
1279 match &**slice {
1280 PyExpr::Tuple(elts)
1282 if !elts.is_empty()
1283 && !elts.iter().any(|e| matches!(e, PyExpr::Starred(_))) =>
1284 {
1285 format!("{v}[{}]", render_join(elts, pool)?)
1286 }
1287 other => format!("{v}[{}]", render_pycode(other, pool)?),
1288 }
1289 }
1290 })
1291}
1292
1293fn const_text(c: &PyConst, pool: &mut Option<NumPool>) -> Result<String, RenderErr> {
1297 match c {
1298 PyConst::Ellipsis => Ok("...".to_string()),
1299 PyConst::Int(_) | PyConst::Float(_) => {
1300 if let Some(p) = pool.as_mut() {
1301 let tok = p.texts.get(p.idx).cloned();
1302 p.idx += 1;
1303 if let Some(tok) = tok {
1304 if matches!(parse_py_expr(&tok), Ok(PyExpr::Constant(parsed)) if parsed == *c) {
1305 return Ok(tok);
1306 }
1307 }
1308 Err(RenderErr::SourceMismatch)
1309 } else {
1310 Ok(expr::unparse(&PyExpr::Constant(c.clone())))
1311 }
1312 }
1313 PyConst::Str {
1314 value,
1315 quote,
1316 u_prefix: _,
1317 } => Ok(expr::unparse(&PyExpr::Constant(PyConst::Str {
1318 value: value.clone(),
1319 quote: *quote,
1320 u_prefix: false,
1321 }))),
1322 other => Ok(expr::unparse(&PyExpr::Constant(other.clone()))),
1323 }
1324}
1325
1326struct TpParam {
1331 name: String,
1332 kind: ParamKind,
1333 annotation: Option<String>,
1334 default: Option<String>,
1335}
1336
1337struct TpCursor<'t> {
1341 toks: &'t [Tok],
1342 next: usize,
1343 current: Option<usize>,
1344 previous: Option<usize>,
1345}
1346
1347impl<'t> TpCursor<'t> {
1348 fn new(toks: &'t [Tok]) -> Self {
1349 Self {
1350 toks,
1351 next: 0,
1352 current: None,
1353 previous: None,
1354 }
1355 }
1356
1357 fn fetch(&mut self) -> Option<usize> {
1358 self.previous = self.current;
1359 if self.next < self.toks.len() {
1360 self.current = Some(self.next);
1361 self.next += 1;
1362 } else {
1363 self.current = None;
1364 }
1365 self.current
1366 }
1367
1368 fn tok(&self, idx: usize) -> &'t Tok {
1369 &self.toks[idx]
1370 }
1371
1372 fn is_op(&self, idx: Option<usize>, text: &str) -> bool {
1373 idx.is_some_and(|i| self.toks[i].kind == TokKind::Op && self.toks[i].text == text)
1374 }
1375
1376 fn fetch_until_into(&mut self, rdelim: &'static str, out: &mut Vec<usize>) {
1381 let mut expected: Vec<&'static str> = vec![rdelim];
1382 while let Some(i) = self.fetch() {
1383 out.push(i);
1384 let t = self.tok(i);
1385 if t.kind == TokKind::Op {
1386 if expected.last().copied() == Some(t.text.as_str()) {
1387 expected.pop();
1388 if expected.is_empty() {
1389 return;
1390 }
1391 continue;
1392 }
1393 match t.text.as_str() {
1394 "(" => expected.push(")"),
1395 "{" => expected.push("}"),
1396 "[" => expected.push("]"),
1397 _ => {}
1398 }
1399 }
1400 }
1401 }
1402
1403 fn fetch_type_param_spec(&mut self) -> Vec<usize> {
1406 let mut tokens: Vec<usize> = Vec::new();
1407 while let Some(i) = self.fetch() {
1408 tokens.push(i);
1409 let t = self.tok(i);
1410 let mut handled = false;
1411 if t.kind == TokKind::Op {
1412 match t.text.as_str() {
1413 "(" => {
1414 self.fetch_until_into(")", &mut tokens);
1415 handled = true;
1416 }
1417 "{" => {
1418 self.fetch_until_into("}", &mut tokens);
1419 handled = true;
1420 }
1421 "[" => {
1422 self.fetch_until_into("]", &mut tokens);
1423 handled = true;
1424 }
1425 _ => {}
1426 }
1427 }
1428 if !handled && t.kind == TokKind::Op && matches!(t.text.as_str(), ":" | "=" | ",") {
1429 tokens.pop();
1430 break;
1431 }
1432 }
1433 tokens
1434 }
1435}
1436
1437fn tp_parse(toks: &[Tok]) -> Result<Vec<TpParam>, SigParseError> {
1443 let mut cur = TpCursor::new(toks);
1444 let mut out: Vec<TpParam> = Vec::new();
1445 while let Some(i) = cur.fetch() {
1446 if cur.tok(i).kind != TokKind::Name {
1447 continue;
1448 }
1449 let name = cur.tok(i).text.clone();
1450 let kind = if cur.is_op(cur.previous, "*") {
1451 ParamKind::VarPositional
1452 } else if cur.is_op(cur.previous, "**") {
1453 ParamKind::VarKeyword
1454 } else {
1455 ParamKind::PositionalOrKeyword
1456 };
1457
1458 let mut annotation: Option<String> = None;
1459 let mut default: Option<String> = None;
1460 let next = cur.fetch();
1461 if cur.is_op(next, ":") || cur.is_op(next, "=") {
1462 if cur.is_op(next, ":") {
1463 let spec = cur.fetch_type_param_spec();
1464 annotation = Some(build_identifier(&spec, &cur));
1465 }
1466 if cur.is_op(cur.current, "=") {
1467 let spec = cur.fetch_type_param_spec();
1468 default = Some(build_identifier(&spec, &cur));
1469 }
1470 }
1471
1472 if kind != ParamKind::PositionalOrKeyword && annotation.is_some() {
1473 let desc = match kind {
1474 ParamKind::VarPositional => "variadic positional",
1475 ParamKind::VarKeyword => "variadic keyword",
1476 _ => unreachable!("guarded above"),
1477 };
1478 return Err(SigParseError::Syntax(format!(
1479 "type parameter bound or constraint is not allowed for {desc} parameters"
1480 )));
1481 }
1482 out.push(TpParam {
1483 name,
1484 kind,
1485 annotation,
1486 default,
1487 });
1488 }
1489 Ok(out)
1490}
1491
1492fn is_operand_left(t: &Tok) -> bool {
1493 matches!(t.kind, TokKind::Name | TokKind::Number | TokKind::Str)
1494 || (t.kind == TokKind::Op && matches!(t.text.as_str(), ")" | "]" | "}"))
1495}
1496
1497fn is_operand_right(t: Option<&Tok>) -> bool {
1498 t.is_some_and(|t| {
1499 matches!(t.kind, TokKind::Name | TokKind::Number | TokKind::Str)
1500 || (t.kind == TokKind::Op && matches!(t.text.as_str(), "(" | "[" | "{"))
1501 })
1502}
1503
1504fn build_identifier(spec: &[usize], cur: &TpCursor<'_>) -> String {
1511 let toks: Vec<&Tok> = spec.iter().map(|&i| cur.tok(i)).collect();
1512 let mut idents: Vec<String> = Vec::new();
1513 let mut pos = 0usize;
1514 while pos < toks.len()
1515 && toks[pos].kind == TokKind::Op
1516 && matches!(toks[pos].text.as_str(), "(" | "[" | "{")
1517 {
1518 idents.push(toks[pos].text.clone());
1519 pos += 1;
1520 }
1521 if pos < toks.len() {
1522 let first = toks[pos];
1523 let is_unpack = first.kind == TokKind::Op && first.text == "*";
1524 idents.push(tp_pformat_token(first, is_unpack));
1525 pos += 1;
1526 }
1527 let rest = &toks[pos..];
1528 let mut is_unpack = false;
1529 for (j, tok) in rest.iter().enumerate() {
1530 idents.push(tp_pformat_token(tok, is_unpack));
1531 let op = rest.get(j + 1);
1532 let after = rest.get(j + 2).copied();
1533 is_unpack = op
1534 .is_some_and(|o| o.kind == TokKind::Op && matches!(o.text.as_str(), "*" | "**"))
1535 && !(is_operand_left(tok) && is_operand_right(after));
1536 }
1537 idents.concat().trim().to_string()
1538}
1539
1540fn tp_pformat_token(tok: &Tok, native: bool) -> String {
1542 if native {
1543 return tok.text.clone();
1544 }
1545 if tok.kind == TokKind::Op {
1546 if matches!(tok.text.as_str(), ":" | "," | "#") {
1547 return format!("{} ", tok.text);
1548 }
1549 if matches!(
1550 tok.text.as_str(),
1551 "=" | "|"
1552 | "&"
1553 | "^"
1554 | "<"
1555 | ">"
1556 | "+"
1557 | "-"
1558 | "*"
1559 | "**"
1560 | "@"
1561 | "/"
1562 | "//"
1563 | "%"
1564 | "<<"
1565 | ">>"
1566 | ">>>"
1567 | "<="
1568 | ">="
1569 | "=="
1570 | "!="
1571 ) {
1572 return format!(" {} ", tok.text);
1573 }
1574 }
1575 tok.text.clone()
1576}
1577
1578#[cfg(test)]
1579mod tests {
1580 use super::*;
1581
1582 fn dctx() -> PyRefContext {
1583 PyRefContext::default()
1584 }
1585
1586 fn dcfg() -> PySigConfig {
1587 PySigConfig::default()
1588 }
1589
1590 fn parsed(arglist: &str) -> String {
1591 parse_arglist(arglist, false, &dctx(), &dcfg())
1592 .expect("arglist parses")
1593 .pformat()
1594 }
1595
1596 fn pseudo(arglist: &str) -> String {
1597 pseudo_parse_arglist(arglist, false, &dctx(), &dcfg()).pformat()
1598 }
1599
1600 fn tp(tp_list: &str) -> String {
1601 parse_type_list(tp_list, false, &dctx(), &dcfg())
1602 .expect("tp list parses")
1603 .pformat()
1604 }
1605
1606 fn kinds_of(arglist: &str) -> Vec<ParamKind> {
1607 signature_from_str(arglist)
1608 .expect("arglist parses")
1609 .into_iter()
1610 .map(|p| p.kind)
1611 .collect()
1612 }
1613
1614 fn sig_match(tp_span: (usize, usize), arg_span: (usize, usize)) -> PySigMatch {
1615 PySigMatch {
1616 prefix: None,
1617 name: String::new(),
1618 tp_list: None,
1619 arglist: None,
1620 retann: None,
1621 tp_span,
1622 arg_span,
1623 }
1624 }
1625
1626 fn cfg_max(global: i64) -> PySigConfig {
1627 PySigConfig {
1628 maximum_signature_line_length: Some(global),
1629 ..PySigConfig::default()
1630 }
1631 }
1632
1633 fn flags(
1635 sig: &str,
1636 tp_span: (usize, usize),
1637 arg_span: (usize, usize),
1638 max: i64,
1639 ) -> (bool, bool) {
1640 multi_line_flags(
1641 sig,
1642 &sig_match(tp_span, arg_span),
1643 SingleLineOpts::default(),
1644 &cfg_max(max),
1645 )
1646 }
1647
1648 const PL_HEAD: &str = "<desc_parameterlist multi_line_parameter_list=\"0\" multi_line_trailing_comma=\"1\" xml:space=\"preserve\">\n";
1650 const TPL_HEAD: &str = "<desc_type_parameter_list multi_line_parameter_list=\"0\" multi_line_trailing_comma=\"1\" xml:space=\"preserve\">\n";
1651
1652 #[test]
1656 fn sig_empty_arglist_is_no_params() {
1657 assert_eq!(signature_from_str(""), Ok(Vec::new()));
1658 assert_eq!(signature_from_str(" "), Ok(Vec::new()));
1659 }
1660
1661 #[test]
1664 fn sig_plain_params_are_positional_or_keyword() {
1665 assert_eq!(
1666 signature_from_str("a, b").unwrap(),
1667 vec![
1668 PyParam {
1669 name: "a".to_string(),
1670 kind: ParamKind::PositionalOrKeyword,
1671 annotation: None,
1672 default: None,
1673 },
1674 PyParam {
1675 name: "b".to_string(),
1676 kind: ParamKind::PositionalOrKeyword,
1677 annotation: None,
1678 default: None,
1679 },
1680 ]
1681 );
1682 }
1683
1684 #[test]
1687 fn sig_full_marker_classification() {
1688 assert_eq!(
1689 signature_from_str("a, b=1, *args, c: int = 2, **kwargs").unwrap(),
1690 vec![
1691 PyParam {
1692 name: "a".to_string(),
1693 kind: ParamKind::PositionalOrKeyword,
1694 annotation: None,
1695 default: None,
1696 },
1697 PyParam {
1698 name: "b".to_string(),
1699 kind: ParamKind::PositionalOrKeyword,
1700 annotation: None,
1701 default: Some("1".to_string()),
1702 },
1703 PyParam {
1704 name: "args".to_string(),
1705 kind: ParamKind::VarPositional,
1706 annotation: None,
1707 default: None,
1708 },
1709 PyParam {
1710 name: "c".to_string(),
1711 kind: ParamKind::KeywordOnly,
1712 annotation: Some("int".to_string()),
1713 default: Some("2".to_string()),
1714 },
1715 PyParam {
1716 name: "kwargs".to_string(),
1717 kind: ParamKind::VarKeyword,
1718 annotation: None,
1719 default: None,
1720 },
1721 ]
1722 );
1723 }
1724
1725 #[test]
1728 fn sig_slash_and_star_zones() {
1729 assert_eq!(
1730 kinds_of("a, /, b, *, c"),
1731 vec![
1732 ParamKind::PositionalOnly,
1733 ParamKind::PositionalOrKeyword,
1734 ParamKind::KeywordOnly,
1735 ]
1736 );
1737 assert_eq!(kinds_of("a, /"), vec![ParamKind::PositionalOnly]);
1738 assert_eq!(
1739 kinds_of("a, *args, b"),
1740 vec![
1741 ParamKind::PositionalOrKeyword,
1742 ParamKind::VarPositional,
1743 ParamKind::KeywordOnly,
1744 ]
1745 );
1746 assert_eq!(kinds_of("*, a"), vec![ParamKind::KeywordOnly]);
1747 }
1748
1749 #[test]
1754 fn sig_duplicate_names_are_duplicate_errors() {
1755 for arglist in ["a, a", "a, /, a", "a, *a", "a, **a", "a, *, a"] {
1756 assert_eq!(
1757 signature_from_str(arglist),
1758 Err(SigParseError::Duplicate("a".to_string())),
1759 "arglist {arglist:?}"
1760 );
1761 }
1762 assert_eq!(
1764 signature_from_str("x, y, y, x"),
1765 Err(SigParseError::Duplicate("y".to_string()))
1766 );
1767 }
1768
1769 #[test]
1772 fn sig_grammar_violations_are_syntax_errors() {
1773 for arglist in [
1774 "/",
1775 "/, a",
1776 "a, /, b, /",
1777 "*",
1778 "a, *,",
1779 "*, **kw",
1780 "**kw, a",
1781 "*a, *b",
1782 "*args, /",
1783 ",",
1784 "a, , b",
1785 "*args=1",
1786 "**kw=2",
1787 "a=1, b",
1788 "a=1, /, b",
1789 "a b",
1790 "if",
1791 "None",
1792 "x.y",
1793 "x()",
1794 "42",
1795 ] {
1796 assert!(
1797 matches!(signature_from_str(arglist), Err(SigParseError::Syntax(_))),
1798 "arglist {arglist:?}"
1799 );
1800 }
1801 }
1802
1803 #[test]
1807 fn sig_keyword_only_zone_allows_default_gaps() {
1808 assert!(signature_from_str("a=1, *, b").is_ok());
1809 assert!(signature_from_str("*, a=1, b").is_ok());
1810 assert!(signature_from_str("a=1, *args, b").is_ok());
1811 assert!(signature_from_str("a=1, /, b=2").is_ok());
1812 }
1813
1814 #[test]
1816 fn sig_trailing_comma_allowed() {
1817 assert_eq!(kinds_of("a,").len(), 1);
1818 assert_eq!(kinds_of("**kw, ").len(), 1);
1819 assert_eq!(kinds_of("*args,").len(), 1);
1820 }
1821
1822 #[test]
1828 fn sig_defaults_keep_numeric_source_text() {
1829 let params = signature_from_str("x=0xFF, y=[1,2], z=1_000, w=1e5").unwrap();
1830 let defaults: Vec<&str> = params
1831 .iter()
1832 .map(|p| p.default.as_deref().unwrap())
1833 .collect();
1834 assert_eq!(defaults, vec!["0xFF", "[1, 2]", "1_000", "1e5"]);
1835 }
1836
1837 #[test]
1841 fn sig_defaults_use_pycode_unparse_rules() {
1842 let params = signature_from_str(
1843 "a=(x+y)*z, b=x**y, c=u'v', d='s', e=-1, f=~x, g=(1,), h={1: 2}, i={3}, j=f2(4, k=5), k=x[1:2:3]",
1844 );
1845 assert!(matches!(params, Err(SigParseError::Syntax(_))));
1847 let params = signature_from_str(
1848 "a=(x+y)*z, b=x**y, c=u'v', d='s', e=-1, f=~x, g=(1,), h={1: 2}, i={3}, j=f2(4, k=5)",
1849 )
1850 .unwrap();
1851 let defaults: Vec<&str> = params
1852 .iter()
1853 .map(|p| p.default.as_deref().unwrap())
1854 .collect();
1855 assert_eq!(
1856 defaults,
1857 vec![
1858 "x + y * z",
1859 "x**y",
1860 "'v'",
1861 "'s'",
1862 "-1",
1863 "~x",
1864 "(1,)",
1865 "{1: 2}",
1866 "{3}",
1867 "f2(4, k=5)",
1868 ]
1869 );
1870 }
1871
1872 #[test]
1875 fn sig_annotations_are_pycode_normalized() {
1876 let params = signature_from_str("x: dict[str,int], y: 'T'").unwrap();
1877 assert_eq!(params[0].annotation.as_deref(), Some("dict[str, int]"));
1878 assert_eq!(params[1].annotation.as_deref(), Some("'T'"));
1879 }
1880
1881 #[test]
1884 fn sig_string_protected_comma() {
1885 let params = signature_from_str("x='a,b'").unwrap();
1886 assert_eq!(params.len(), 1);
1887 assert_eq!(params[0].default.as_deref(), Some("'a,b'"));
1888 }
1889
1890 #[test]
1894 fn sig_top_level_lambda_is_syntax() {
1895 assert!(matches!(
1896 signature_from_str("x=lambda a, b: 0"),
1897 Err(SigParseError::Syntax(_))
1898 ));
1899 }
1900
1901 #[test]
1906 fn matrix_a_measurement() {
1907 assert_eq!(flags("foo(aaaa)", (0, 0), (4, 8), 9), (false, false));
1909 assert_eq!(flags("foo(aaaa)", (0, 0), (4, 8), 8), (true, false));
1911 assert_eq!(flags("foo(a) -> int", (0, 0), (4, 5), 12), (true, false));
1913 assert_eq!(flags("foo(a) -> int", (0, 0), (4, 5), 13), (false, false));
1915 assert_eq!(flags("Klass.foo(a)", (0, 0), (10, 11), 11), (true, false));
1917 assert_eq!(flags("foo(aaaa)", (0, 0), (4, 8), 9), (false, false));
1920 }
1921
1922 #[test]
1926 fn matrix_b_span_subtraction() {
1927 assert_eq!(flags("foo[T](aaaa)", (4, 5), (7, 11), 10), (true, false));
1929 assert_eq!(flags("foo[T](aaaa)", (4, 5), (7, 11), 7), (true, true));
1931 assert_eq!(flags("foo[T](aaaa)", (4, 5), (7, 11), 11), (false, false));
1933 }
1934
1935 #[test]
1939 fn matrix_c_config_precedence() {
1940 let m = sig_match((0, 0), (4, 8));
1941 let opts = SingleLineOpts::default();
1942 let run = |python: Option<i64>, global: Option<i64>| {
1943 let cfg = PySigConfig {
1944 python_maximum_signature_line_length: python,
1945 maximum_signature_line_length: global,
1946 ..PySigConfig::default()
1947 };
1948 multi_line_flags("foo(aaaa)", &m, opts, &cfg).0
1949 };
1950 assert!(!run(Some(1000), Some(1))); assert!(run(Some(1), Some(1000))); assert!(run(None, Some(1))); assert!(run(Some(0), Some(1))); assert!(!run(None, None)); }
1956
1957 #[test]
1960 fn matrix_d_single_line_options_are_independent() {
1961 let m = sig_match((4, 5), (7, 11));
1962 let cfg = cfg_max(1);
1963 assert_eq!(
1964 multi_line_flags(
1965 "foo[T](aaaa)",
1966 &m,
1967 SingleLineOpts {
1968 parameter_list: true,
1969 type_parameter_list: false,
1970 },
1971 &cfg
1972 ),
1973 (false, true)
1974 );
1975 assert_eq!(
1976 multi_line_flags(
1977 "foo[T](aaaa)",
1978 &m,
1979 SingleLineOpts {
1980 parameter_list: false,
1981 type_parameter_list: true,
1982 },
1983 &cfg
1984 ),
1985 (true, false)
1986 );
1987 }
1988
1989 #[test]
1994 fn arglist_plain_params() {
1995 assert_eq!(
1996 parsed("a, b"),
1997 [
1998 PL_HEAD,
1999 concat!(
2000 " <desc_parameter xml:space=\"preserve\">\n",
2001 " <desc_sig_name classes=\"n\">\n",
2002 " a\n",
2003 " <desc_parameter xml:space=\"preserve\">\n",
2004 " <desc_sig_name classes=\"n\">\n",
2005 " b\n",
2006 )
2007 ]
2008 .concat()
2009 );
2010 }
2011
2012 #[test]
2016 fn arglist_full_markers() {
2017 assert_eq!(
2018 parsed("a, b=1, *args, c: int = 2, **kwargs"),
2019 [
2020 PL_HEAD,
2021 concat!(
2022 " <desc_parameter xml:space=\"preserve\">\n",
2023 " <desc_sig_name classes=\"n\">\n",
2024 " a\n",
2025 " <desc_parameter xml:space=\"preserve\">\n",
2026 " <desc_sig_name classes=\"n\">\n",
2027 " b\n",
2028 " <desc_sig_operator classes=\"o\">\n",
2029 " =\n",
2030 " <inline classes=\"default_value\" support_smartquotes=\"0\">\n",
2031 " 1\n",
2032 " <desc_parameter xml:space=\"preserve\">\n",
2033 " <desc_sig_operator classes=\"o\">\n",
2034 " *\n",
2035 " <desc_sig_name classes=\"n\">\n",
2036 " args\n",
2037 " <desc_parameter xml:space=\"preserve\">\n",
2038 " <desc_sig_name classes=\"n\">\n",
2039 " c\n",
2040 " <desc_sig_punctuation classes=\"p\">\n",
2041 " :\n",
2042 " <desc_sig_space classes=\"w\">\n",
2043 " \n",
2044 " <desc_sig_name classes=\"n\">\n",
2045 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
2046 " int\n",
2047 " <desc_sig_space classes=\"w\">\n",
2048 " \n",
2049 " <desc_sig_operator classes=\"o\">\n",
2050 " =\n",
2051 " <desc_sig_space classes=\"w\">\n",
2052 " \n",
2053 " <inline classes=\"default_value\" support_smartquotes=\"0\">\n",
2054 " 2\n",
2055 " <desc_parameter xml:space=\"preserve\">\n",
2056 " <desc_sig_operator classes=\"o\">\n",
2057 " **\n",
2058 " <desc_sig_name classes=\"n\">\n",
2059 " kwargs\n",
2060 )
2061 ]
2062 .concat()
2063 );
2064 }
2065
2066 #[test]
2070 fn arglist_separator_shapes() {
2071 assert_eq!(
2072 parsed("a, /, b, *, c"),
2073 [
2074 PL_HEAD,
2075 concat!(
2076 " <desc_parameter xml:space=\"preserve\">\n",
2077 " <desc_sig_name classes=\"n\">\n",
2078 " a\n",
2079 " <desc_parameter xml:space=\"preserve\">\n",
2080 " <desc_sig_operator classes=\"positional-only-separator o\">\n",
2081 " <abbreviation explanation=\"Positional-only parameter separator (PEP 570)\">\n",
2082 " /\n",
2083 " <desc_parameter xml:space=\"preserve\">\n",
2084 " <desc_sig_name classes=\"n\">\n",
2085 " b\n",
2086 " <desc_parameter xml:space=\"preserve\">\n",
2087 " <desc_sig_operator classes=\"keyword-only-separator o\">\n",
2088 " <abbreviation explanation=\"Keyword-only parameters separator (PEP 3102)\">\n",
2089 " *\n",
2090 " <desc_parameter xml:space=\"preserve\">\n",
2091 " <desc_sig_name classes=\"n\">\n",
2092 " c\n",
2093 )
2094 ]
2095 .concat()
2096 );
2097 }
2098
2099 #[test]
2102 fn arglist_trailing_slash_epilogue() {
2103 assert_eq!(
2104 parsed("a, /"),
2105 [
2106 PL_HEAD,
2107 concat!(
2108 " <desc_parameter xml:space=\"preserve\">\n",
2109 " <desc_sig_name classes=\"n\">\n",
2110 " a\n",
2111 " <desc_parameter xml:space=\"preserve\">\n",
2112 " <desc_sig_operator classes=\"positional-only-separator o\">\n",
2113 " <abbreviation explanation=\"Positional-only parameter separator (PEP 570)\">\n",
2114 " /\n",
2115 )
2116 ]
2117 .concat()
2118 );
2119 }
2120
2121 #[test]
2125 fn arglist_adjacent_separators() {
2126 assert_eq!(
2127 parsed("a, /, *, b"),
2128 [
2129 PL_HEAD,
2130 concat!(
2131 " <desc_parameter xml:space=\"preserve\">\n",
2132 " <desc_sig_name classes=\"n\">\n",
2133 " a\n",
2134 " <desc_parameter xml:space=\"preserve\">\n",
2135 " <desc_sig_operator classes=\"positional-only-separator o\">\n",
2136 " <abbreviation explanation=\"Positional-only parameter separator (PEP 570)\">\n",
2137 " /\n",
2138 " <desc_parameter xml:space=\"preserve\">\n",
2139 " <desc_sig_operator classes=\"keyword-only-separator o\">\n",
2140 " <abbreviation explanation=\"Keyword-only parameters separator (PEP 3102)\">\n",
2141 " *\n",
2142 " <desc_parameter xml:space=\"preserve\">\n",
2143 " <desc_sig_name classes=\"n\">\n",
2144 " b\n",
2145 )
2146 ]
2147 .concat()
2148 );
2149 }
2150
2151 #[test]
2154 fn arglist_kwonly_separator_first() {
2155 assert_eq!(
2156 parsed("*, a"),
2157 [
2158 PL_HEAD,
2159 concat!(
2160 " <desc_parameter xml:space=\"preserve\">\n",
2161 " <desc_sig_operator classes=\"keyword-only-separator o\">\n",
2162 " <abbreviation explanation=\"Keyword-only parameters separator (PEP 3102)\">\n",
2163 " *\n",
2164 " <desc_parameter xml:space=\"preserve\">\n",
2165 " <desc_sig_name classes=\"n\">\n",
2166 " a\n",
2167 )
2168 ]
2169 .concat()
2170 );
2171 }
2172
2173 #[test]
2176 fn arglist_no_separator_after_varargs() {
2177 assert_eq!(
2178 parsed("a, *args, b"),
2179 [
2180 PL_HEAD,
2181 concat!(
2182 " <desc_parameter xml:space=\"preserve\">\n",
2183 " <desc_sig_name classes=\"n\">\n",
2184 " a\n",
2185 " <desc_parameter xml:space=\"preserve\">\n",
2186 " <desc_sig_operator classes=\"o\">\n",
2187 " *\n",
2188 " <desc_sig_name classes=\"n\">\n",
2189 " args\n",
2190 " <desc_parameter xml:space=\"preserve\">\n",
2191 " <desc_sig_name classes=\"n\">\n",
2192 " b\n",
2193 )
2194 ]
2195 .concat()
2196 );
2197 }
2198
2199 #[test]
2202 fn arglist_default_value_text() {
2203 assert_eq!(
2204 parsed("name='x', items=[]"),
2205 [
2206 PL_HEAD,
2207 concat!(
2208 " <desc_parameter xml:space=\"preserve\">\n",
2209 " <desc_sig_name classes=\"n\">\n",
2210 " name\n",
2211 " <desc_sig_operator classes=\"o\">\n",
2212 " =\n",
2213 " <inline classes=\"default_value\" support_smartquotes=\"0\">\n",
2214 " 'x'\n",
2215 " <desc_parameter xml:space=\"preserve\">\n",
2216 " <desc_sig_name classes=\"n\">\n",
2217 " items\n",
2218 " <desc_sig_operator classes=\"o\">\n",
2219 " =\n",
2220 " <inline classes=\"default_value\" support_smartquotes=\"0\">\n",
2221 " []\n",
2222 )
2223 ]
2224 .concat()
2225 );
2226 assert_eq!(
2227 parsed("x=0xFF, y=[1,2]"),
2228 [
2229 PL_HEAD,
2230 concat!(
2231 " <desc_parameter xml:space=\"preserve\">\n",
2232 " <desc_sig_name classes=\"n\">\n",
2233 " x\n",
2234 " <desc_sig_operator classes=\"o\">\n",
2235 " =\n",
2236 " <inline classes=\"default_value\" support_smartquotes=\"0\">\n",
2237 " 0xFF\n",
2238 " <desc_parameter xml:space=\"preserve\">\n",
2239 " <desc_sig_name classes=\"n\">\n",
2240 " y\n",
2241 " <desc_sig_operator classes=\"o\">\n",
2242 " =\n",
2243 " <inline classes=\"default_value\" support_smartquotes=\"0\">\n",
2244 " [1, 2]\n",
2245 )
2246 ]
2247 .concat()
2248 );
2249 }
2250
2251 #[test]
2254 fn arglist_annotated_default_is_space_wrapped() {
2255 assert_eq!(
2256 parsed("x: int = 2"),
2257 [
2258 PL_HEAD,
2259 concat!(
2260 " <desc_parameter xml:space=\"preserve\">\n",
2261 " <desc_sig_name classes=\"n\">\n",
2262 " x\n",
2263 " <desc_sig_punctuation classes=\"p\">\n",
2264 " :\n",
2265 " <desc_sig_space classes=\"w\">\n",
2266 " \n",
2267 " <desc_sig_name classes=\"n\">\n",
2268 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
2269 " int\n",
2270 " <desc_sig_space classes=\"w\">\n",
2271 " \n",
2272 " <desc_sig_operator classes=\"o\">\n",
2273 " =\n",
2274 " <desc_sig_space classes=\"w\">\n",
2275 " \n",
2276 " <inline classes=\"default_value\" support_smartquotes=\"0\">\n",
2277 " 2\n",
2278 )
2279 ]
2280 .concat()
2281 );
2282 }
2283
2284 #[test]
2287 fn arglist_string_comma_default() {
2288 assert_eq!(
2289 parsed("x='a,b'"),
2290 [
2291 PL_HEAD,
2292 concat!(
2293 " <desc_parameter xml:space=\"preserve\">\n",
2294 " <desc_sig_name classes=\"n\">\n",
2295 " x\n",
2296 " <desc_sig_operator classes=\"o\">\n",
2297 " =\n",
2298 " <inline classes=\"default_value\" support_smartquotes=\"0\">\n",
2299 " 'a,b'\n",
2300 )
2301 ]
2302 .concat()
2303 );
2304 }
2305
2306 #[test]
2313 fn arglist_empty_still_carries_attrs() {
2314 assert_eq!(parsed(""), PL_HEAD);
2315 assert_eq!(parsed(" "), PL_HEAD);
2316 }
2317
2318 #[test]
2323 fn arglist_attr_values_follow_inputs() {
2324 let flipped = parse_arglist("aaaa", true, &dctx(), &dcfg())
2325 .unwrap()
2326 .pformat();
2327 assert!(flipped.starts_with(
2328 "<desc_parameterlist multi_line_parameter_list=\"1\" multi_line_trailing_comma=\"1\""
2329 ));
2330 let no_comma_cfg = PySigConfig {
2331 python_trailing_comma_in_multi_line_signatures: false,
2332 ..PySigConfig::default()
2333 };
2334 let no_comma = parse_arglist("aaaa", true, &dctx(), &no_comma_cfg)
2335 .unwrap()
2336 .pformat();
2337 assert!(no_comma.starts_with(
2338 "<desc_parameterlist multi_line_parameter_list=\"1\" multi_line_trailing_comma=\"0\""
2339 ));
2340 }
2341
2342 #[test]
2345 fn arglist_error_channels() {
2346 assert_eq!(
2347 parse_arglist("a, a", false, &dctx(), &dcfg()),
2348 Err(SigParseError::Duplicate("a".to_string()))
2349 );
2350 assert!(matches!(
2351 parse_arglist("a[, b]", false, &dctx(), &dcfg()),
2352 Err(SigParseError::Syntax(_))
2353 ));
2354 }
2355
2356 #[test]
2359 fn matrix_span_edge_cases() {
2360 assert_eq!(flags("foo(aaaa)", (5, 2), (4, 8), 8), (true, false));
2361 assert_eq!(flags("foo(aaaa)", (3, 999), (4, 8), 8), (true, false));
2362 assert_eq!(
2364 flags("f\u{f3}\u{f3}(aaaa)", (0, 0), (6, 10), 9),
2365 (false, false)
2366 );
2367 assert_eq!(
2368 flags("f\u{f3}\u{f3}(aaaa)", (0, 0), (6, 10), 8),
2369 (true, false)
2370 );
2371 }
2372
2373 #[test]
2378 fn pseudo_brackets_become_optional() {
2379 assert_eq!(
2380 pseudo("a[, b]"),
2381 [
2382 PL_HEAD,
2383 concat!(
2384 " <desc_parameter xml:space=\"preserve\">\n",
2385 " <desc_sig_name classes=\"n\">\n",
2386 " a\n",
2387 " <desc_optional xml:space=\"preserve\">\n",
2388 " <desc_parameter xml:space=\"preserve\">\n",
2389 " <desc_sig_name classes=\"n\">\n",
2390 " b\n",
2391 )
2392 ]
2393 .concat()
2394 );
2395 }
2396
2397 #[test]
2399 fn pseudo_nested_optionals() {
2400 assert_eq!(
2401 pseudo("a[, b[, c]]"),
2402 [
2403 PL_HEAD,
2404 concat!(
2405 " <desc_parameter xml:space=\"preserve\">\n",
2406 " <desc_sig_name classes=\"n\">\n",
2407 " a\n",
2408 " <desc_optional xml:space=\"preserve\">\n",
2409 " <desc_parameter xml:space=\"preserve\">\n",
2410 " <desc_sig_name classes=\"n\">\n",
2411 " b\n",
2412 " <desc_optional xml:space=\"preserve\">\n",
2413 " <desc_parameter xml:space=\"preserve\">\n",
2414 " <desc_sig_name classes=\"n\">\n",
2415 " c\n",
2416 )
2417 ]
2418 .concat()
2419 );
2420 }
2421
2422 #[test]
2425 fn pseudo_bare_equals_when_unannotated() {
2426 assert_eq!(
2427 pseudo("n=1"),
2428 [
2429 PL_HEAD,
2430 concat!(
2431 " <desc_parameter xml:space=\"preserve\">\n",
2432 " <desc_sig_name classes=\"n\">\n",
2433 " n\n",
2434 " <desc_sig_operator classes=\"o\">\n",
2435 " =\n",
2436 " <inline classes=\"default_value\" support_smartquotes=\"0\">\n",
2437 " 1\n",
2438 )
2439 ]
2440 .concat()
2441 );
2442 }
2443
2444 #[test]
2448 fn pseudo_annotated_default() {
2449 assert_eq!(
2450 pseudo("x: int=2[, y]"),
2451 [
2452 PL_HEAD,
2453 concat!(
2454 " <desc_parameter xml:space=\"preserve\">\n",
2455 " <desc_sig_name classes=\"n\">\n",
2456 " x\n",
2457 " <desc_sig_punctuation classes=\"p\">\n",
2458 " :\n",
2459 " <desc_sig_space classes=\"w\">\n",
2460 " \n",
2461 " <desc_sig_name classes=\"n\">\n",
2462 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
2463 " int\n",
2464 " <desc_sig_space classes=\"w\">\n",
2465 " \n",
2466 " <desc_sig_operator classes=\"o\">\n",
2467 " =\n",
2468 " <desc_sig_space classes=\"w\">\n",
2469 " \n",
2470 " <inline classes=\"default_value\" support_smartquotes=\"0\">\n",
2471 " 2\n",
2472 " <desc_optional xml:space=\"preserve\">\n",
2473 " <desc_parameter xml:space=\"preserve\">\n",
2474 " <desc_sig_name classes=\"n\">\n",
2475 " y\n",
2476 )
2477 ]
2478 .concat()
2479 );
2480 }
2481
2482 #[test]
2486 fn pseudo_attrs_are_unconditional() {
2487 assert_eq!(
2488 pseudo("a, a"),
2489 [
2490 PL_HEAD,
2491 concat!(
2492 " <desc_parameter xml:space=\"preserve\">\n",
2493 " <desc_sig_name classes=\"n\">\n",
2494 " a\n",
2495 " <desc_parameter xml:space=\"preserve\">\n",
2496 " <desc_sig_name classes=\"n\">\n",
2497 " a\n",
2498 )
2499 ]
2500 .concat()
2501 );
2502 }
2503
2504 #[test]
2507 fn pseudo_star_names_kept() {
2508 assert_eq!(
2509 pseudo("*args, **kw"),
2510 [
2511 PL_HEAD,
2512 concat!(
2513 " <desc_parameter xml:space=\"preserve\">\n",
2514 " <desc_sig_name classes=\"n\">\n",
2515 " *args\n",
2516 " <desc_parameter xml:space=\"preserve\">\n",
2517 " <desc_sig_name classes=\"n\">\n",
2518 " **kw\n",
2519 )
2520 ]
2521 .concat()
2522 );
2523 }
2524
2525 #[test]
2530 fn pseudo_imbalance_gives_attrless_raw_parameter() {
2531 assert_eq!(
2532 pseudo("a[, b"),
2533 concat!(
2534 "<desc_parameterlist xml:space=\"preserve\">\n",
2535 " <desc_parameter xml:space=\"preserve\">\n",
2536 " a[, b\n",
2537 )
2538 );
2539 assert_eq!(
2541 pseudo("a], b"),
2542 concat!(
2543 "<desc_parameterlist xml:space=\"preserve\">\n",
2544 " <desc_parameter xml:space=\"preserve\">\n",
2545 " a], b\n",
2546 )
2547 );
2548 }
2549
2550 #[test]
2555 fn tp_plain_and_variadic_params() {
2556 assert_eq!(
2557 tp("T, *Ts, **P"),
2558 [
2559 TPL_HEAD,
2560 concat!(
2561 " <desc_type_parameter xml:space=\"preserve\">\n",
2562 " <desc_sig_name classes=\"n\">\n",
2563 " T\n",
2564 " <desc_type_parameter xml:space=\"preserve\">\n",
2565 " <desc_sig_operator classes=\"o\">\n",
2566 " *\n",
2567 " <desc_sig_name classes=\"n\">\n",
2568 " Ts\n",
2569 " <desc_type_parameter xml:space=\"preserve\">\n",
2570 " <desc_sig_operator classes=\"o\">\n",
2571 " **\n",
2572 " <desc_sig_name classes=\"n\">\n",
2573 " P\n",
2574 )
2575 ]
2576 .concat()
2577 );
2578 }
2579
2580 #[test]
2583 fn tp_bound() {
2584 assert_eq!(
2585 tp("T: int"),
2586 [
2587 TPL_HEAD,
2588 concat!(
2589 " <desc_type_parameter xml:space=\"preserve\">\n",
2590 " <desc_sig_name classes=\"n\">\n",
2591 " T\n",
2592 " <desc_sig_punctuation classes=\"p\">\n",
2593 " :\n",
2594 " <desc_sig_space classes=\"w\">\n",
2595 " \n",
2596 " <desc_sig_name classes=\"n\">\n",
2597 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
2598 " int\n",
2599 )
2600 ]
2601 .concat()
2602 );
2603 }
2604
2605 #[test]
2609 fn tp_constraint_reparenthesized() {
2610 assert_eq!(
2611 tp("T: (int, str)"),
2612 [
2613 TPL_HEAD,
2614 concat!(
2615 " <desc_type_parameter xml:space=\"preserve\">\n",
2616 " <desc_sig_name classes=\"n\">\n",
2617 " T\n",
2618 " <desc_sig_punctuation classes=\"p\">\n",
2619 " :\n",
2620 " <desc_sig_space classes=\"w\">\n",
2621 " \n",
2622 " <desc_sig_punctuation classes=\"p\">\n",
2623 " (\n",
2624 " <desc_sig_name classes=\"n\">\n",
2625 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
2626 " int\n",
2627 " <desc_sig_punctuation classes=\"p\">\n",
2628 " ,\n",
2629 " <desc_sig_space classes=\"w\">\n",
2630 " \n",
2631 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"str\" reftype=\"class\">\n",
2632 " str\n",
2633 " <desc_sig_punctuation classes=\"p\">\n",
2634 " )\n",
2635 )
2636 ]
2637 .concat()
2638 );
2639 }
2640
2641 #[test]
2645 fn tp_default() {
2646 assert_eq!(
2647 tp("T = int"),
2648 [
2649 TPL_HEAD,
2650 concat!(
2651 " <desc_type_parameter xml:space=\"preserve\">\n",
2652 " <desc_sig_name classes=\"n\">\n",
2653 " T\n",
2654 " <desc_sig_space classes=\"w\">\n",
2655 " \n",
2656 " <desc_sig_operator classes=\"o\">\n",
2657 " =\n",
2658 " <desc_sig_space classes=\"w\">\n",
2659 " \n",
2660 " <inline classes=\"default_value\" support_smartquotes=\"0\">\n",
2661 " int\n",
2662 )
2663 ]
2664 .concat()
2665 );
2666 }
2667
2668 #[test]
2670 fn tp_bound_and_default() {
2671 assert_eq!(
2672 tp("T: int = str"),
2673 [
2674 TPL_HEAD,
2675 concat!(
2676 " <desc_type_parameter xml:space=\"preserve\">\n",
2677 " <desc_sig_name classes=\"n\">\n",
2678 " T\n",
2679 " <desc_sig_punctuation classes=\"p\">\n",
2680 " :\n",
2681 " <desc_sig_space classes=\"w\">\n",
2682 " \n",
2683 " <desc_sig_name classes=\"n\">\n",
2684 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
2685 " int\n",
2686 " <desc_sig_space classes=\"w\">\n",
2687 " \n",
2688 " <desc_sig_operator classes=\"o\">\n",
2689 " =\n",
2690 " <desc_sig_space classes=\"w\">\n",
2691 " \n",
2692 " <inline classes=\"default_value\" support_smartquotes=\"0\">\n",
2693 " str\n",
2694 )
2695 ]
2696 .concat()
2697 );
2698 }
2699
2700 #[test]
2704 fn tp_star_default_native_unpack() {
2705 assert_eq!(
2706 tp("*Ts = *tuple[int, ...]"),
2707 [
2708 TPL_HEAD,
2709 concat!(
2710 " <desc_type_parameter xml:space=\"preserve\">\n",
2711 " <desc_sig_operator classes=\"o\">\n",
2712 " *\n",
2713 " <desc_sig_name classes=\"n\">\n",
2714 " Ts\n",
2715 " <desc_sig_space classes=\"w\">\n",
2716 " \n",
2717 " <desc_sig_operator classes=\"o\">\n",
2718 " =\n",
2719 " <desc_sig_space classes=\"w\">\n",
2720 " \n",
2721 " <inline classes=\"default_value\" support_smartquotes=\"0\">\n",
2722 " *tuple[int, ...]\n",
2723 )
2724 ]
2725 .concat()
2726 );
2727 }
2728
2729 #[test]
2732 fn tp_union_bound() {
2733 assert_eq!(
2734 tp("T: int | str"),
2735 [
2736 TPL_HEAD,
2737 concat!(
2738 " <desc_type_parameter xml:space=\"preserve\">\n",
2739 " <desc_sig_name classes=\"n\">\n",
2740 " T\n",
2741 " <desc_sig_punctuation classes=\"p\">\n",
2742 " :\n",
2743 " <desc_sig_space classes=\"w\">\n",
2744 " \n",
2745 " <desc_sig_name classes=\"n\">\n",
2746 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
2747 " int\n",
2748 " <desc_sig_space classes=\"w\">\n",
2749 " \n",
2750 " <desc_sig_punctuation classes=\"p\">\n",
2751 " |\n",
2752 " <desc_sig_space classes=\"w\">\n",
2753 " \n",
2754 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"str\" reftype=\"class\">\n",
2755 " str\n",
2756 )
2757 ]
2758 .concat()
2759 );
2760 }
2761
2762 #[test]
2766 fn tp_variadic_bound_is_error() {
2767 assert_eq!(
2768 parse_type_list("*Ts: int", false, &dctx(), &dcfg()),
2769 Err(SigParseError::Syntax(
2770 "type parameter bound or constraint is not allowed for variadic positional parameters"
2771 .to_string()
2772 ))
2773 );
2774 assert_eq!(
2775 parse_type_list("**P: int", false, &dctx(), &dcfg()),
2776 Err(SigParseError::Syntax(
2777 "type parameter bound or constraint is not allowed for variadic keyword parameters"
2778 .to_string()
2779 ))
2780 );
2781 assert!(parse_type_list("*Ts = 1", false, &dctx(), &dcfg()).is_ok());
2784 }
2785
2786 #[test]
2789 fn tp_unclosed_bracket_is_error() {
2790 assert!(parse_type_list("T: (int", false, &dctx(), &dcfg()).is_err());
2791 }
2792
2793 #[test]
2796 fn tp_attr_values_follow_inputs() {
2797 let flipped = parse_type_list("T", true, &dctx(), &dcfg())
2798 .unwrap()
2799 .pformat();
2800 assert!(flipped.starts_with(
2801 "<desc_type_parameter_list multi_line_parameter_list=\"1\" multi_line_trailing_comma=\"1\""
2802 ));
2803 let no_comma_cfg = PySigConfig {
2804 python_trailing_comma_in_multi_line_signatures: false,
2805 ..PySigConfig::default()
2806 };
2807 let no_comma = parse_type_list("T", true, &dctx(), &no_comma_cfg)
2808 .unwrap()
2809 .pformat();
2810 assert!(no_comma.starts_with(
2811 "<desc_type_parameter_list multi_line_parameter_list=\"1\" multi_line_trailing_comma=\"0\""
2812 ));
2813 }
2814
2815 #[test]
2820 fn sig_negative_defaults_render_flush() {
2821 let params = signature_from_str("x=-1, y=- 2").unwrap();
2822 assert_eq!(params[0].default.as_deref(), Some("-1"));
2823 assert_eq!(params[1].default.as_deref(), Some("-2"));
2824 }
2825
2826 #[test]
2830 fn sig_string_annotation_and_none_default() {
2831 let params = signature_from_str("x: 'A[int]' = None").unwrap();
2832 assert_eq!(params[0].annotation.as_deref(), Some("'A[int]'"));
2833 assert_eq!(params[0].default.as_deref(), Some("None"));
2834 }
2835
2836 #[test]
2839 fn arglist_annotated_variadics() {
2840 assert_eq!(
2841 parsed("*args: int, **kw: str"),
2842 [
2843 PL_HEAD,
2844 concat!(
2845 " <desc_parameter xml:space=\"preserve\">\n",
2846 " <desc_sig_operator classes=\"o\">\n",
2847 " *\n",
2848 " <desc_sig_name classes=\"n\">\n",
2849 " args\n",
2850 " <desc_sig_punctuation classes=\"p\">\n",
2851 " :\n",
2852 " <desc_sig_space classes=\"w\">\n",
2853 " \n",
2854 " <desc_sig_name classes=\"n\">\n",
2855 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
2856 " int\n",
2857 " <desc_parameter xml:space=\"preserve\">\n",
2858 " <desc_sig_operator classes=\"o\">\n",
2859 " **\n",
2860 " <desc_sig_name classes=\"n\">\n",
2861 " kw\n",
2862 " <desc_sig_punctuation classes=\"p\">\n",
2863 " :\n",
2864 " <desc_sig_space classes=\"w\">\n",
2865 " \n",
2866 " <desc_sig_name classes=\"n\">\n",
2867 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"str\" reftype=\"class\">\n",
2868 " str\n",
2869 )
2870 ]
2871 .concat()
2872 );
2873 }
2874
2875 #[test]
2879 fn pseudo_empty_default_is_dropped() {
2880 assert_eq!(
2881 pseudo("a=[, b]"),
2882 [
2883 PL_HEAD,
2884 concat!(
2885 " <desc_parameter xml:space=\"preserve\">\n",
2886 " <desc_sig_name classes=\"n\">\n",
2887 " a\n",
2888 " <desc_optional xml:space=\"preserve\">\n",
2889 " <desc_parameter xml:space=\"preserve\">\n",
2890 " <desc_sig_name classes=\"n\">\n",
2891 " b\n",
2892 )
2893 ]
2894 .concat()
2895 );
2896 }
2897
2898 #[test]
2902 fn sig_string_and_tuple_default_edges() {
2903 let params = signature_from_str("x='a=b', y: str='c:d', z=(1, 2), w=()").unwrap();
2904 assert_eq!(params[0].default.as_deref(), Some("'a=b'"));
2905 assert_eq!(params[1].annotation.as_deref(), Some("str"));
2906 assert_eq!(params[1].default.as_deref(), Some("'c:d'"));
2907 assert_eq!(params[2].default.as_deref(), Some("(1, 2)"));
2908 assert_eq!(params[3].default.as_deref(), Some("()"));
2909 }
2910
2911 #[test]
2916 fn totality_on_garbage_input() {
2917 let horrors = [
2918 "((((((",
2919 "]]]]",
2920 "'unterminated",
2921 "a=(",
2922 "\\",
2923 "\u{1f980}",
2924 "a: :",
2925 "=x",
2926 ":int",
2927 "x=='y'",
2928 "a[b[c[d[",
2929 "\"\"\"",
2930 "0x, 1_, 1e",
2931 "f'{a,b}'",
2932 "., .., ...",
2933 ];
2934 let deep_parens = "(".repeat(10_000);
2935 let deep_brackets = "[, ".repeat(5_000);
2936 for arglist in horrors
2937 .iter()
2938 .copied()
2939 .chain([deep_parens.as_str(), deep_brackets.as_str()])
2940 {
2941 let _ = signature_from_str(arglist);
2942 let _ = parse_arglist(arglist, false, &dctx(), &dcfg());
2943 let _ = pseudo_parse_arglist(arglist, true, &dctx(), &dcfg());
2944 let _ = parse_type_list(arglist, false, &dctx(), &dcfg());
2945 }
2946 }
2947
2948 #[test]
2957 fn pep_646_star_annotation_on_var_positional() {
2958 assert_eq!(
2959 parsed("*args: *Ts"),
2960 concat!(
2961 "<desc_parameterlist multi_line_parameter_list=\"0\" multi_line_trailing_comma=\"1\" xml:space=\"preserve\">\n",
2962 " <desc_parameter xml:space=\"preserve\">\n",
2963 " <desc_sig_operator classes=\"o\">\n",
2964 " *\n",
2965 " <desc_sig_name classes=\"n\">\n",
2966 " args\n",
2967 " <desc_sig_punctuation classes=\"p\">\n",
2968 " :\n",
2969 " <desc_sig_space classes=\"w\">\n",
2970 " \n",
2971 " <desc_sig_name classes=\"n\">\n",
2972 " <desc_sig_operator classes=\"o\">\n",
2973 " *\n",
2974 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"Ts\" reftype=\"class\">\n",
2975 " Ts\n",
2976 )
2977 );
2978 }
2979
2980 #[test]
2988 fn pep_646_star_annotation_over_a_bracketed_unpack() {
2989 assert_eq!(
2990 parsed("*args: *tuple[int, ...]"),
2991 concat!(
2992 "<desc_parameterlist multi_line_parameter_list=\"0\" multi_line_trailing_comma=\"1\" xml:space=\"preserve\">\n",
2993 " <desc_parameter xml:space=\"preserve\">\n",
2994 " <desc_sig_operator classes=\"o\">\n",
2995 " *\n",
2996 " <desc_sig_name classes=\"n\">\n",
2997 " args\n",
2998 " <desc_sig_punctuation classes=\"p\">\n",
2999 " :\n",
3000 " <desc_sig_space classes=\"w\">\n",
3001 " \n",
3002 " <desc_sig_name classes=\"n\">\n",
3003 " <desc_sig_operator classes=\"o\">\n",
3004 " *\n",
3005 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"tuple\" reftype=\"class\">\n",
3006 " tuple\n",
3007 " <desc_sig_punctuation classes=\"p\">\n",
3008 " [\n",
3009 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
3010 " int\n",
3011 " <desc_sig_punctuation classes=\"p\">\n",
3012 " ,\n",
3013 " <desc_sig_space classes=\"w\">\n",
3014 " \n",
3015 " <desc_sig_punctuation classes=\"p\">\n",
3016 " ...\n",
3017 " <desc_sig_punctuation classes=\"p\">\n",
3018 " ]\n",
3019 )
3020 );
3021 }
3022
3023 #[test]
3028 fn star_annotation_leaves_its_neighbours_alone() {
3029 assert_eq!(
3030 parsed("a, *args: *Ts, b"),
3031 concat!(
3032 "<desc_parameterlist multi_line_parameter_list=\"0\" multi_line_trailing_comma=\"1\" xml:space=\"preserve\">\n",
3033 " <desc_parameter xml:space=\"preserve\">\n",
3034 " <desc_sig_name classes=\"n\">\n",
3035 " a\n",
3036 " <desc_parameter xml:space=\"preserve\">\n",
3037 " <desc_sig_operator classes=\"o\">\n",
3038 " *\n",
3039 " <desc_sig_name classes=\"n\">\n",
3040 " args\n",
3041 " <desc_sig_punctuation classes=\"p\">\n",
3042 " :\n",
3043 " <desc_sig_space classes=\"w\">\n",
3044 " \n",
3045 " <desc_sig_name classes=\"n\">\n",
3046 " <desc_sig_operator classes=\"o\">\n",
3047 " *\n",
3048 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"Ts\" reftype=\"class\">\n",
3049 " Ts\n",
3050 " <desc_parameter xml:space=\"preserve\">\n",
3051 " <desc_sig_name classes=\"n\">\n",
3052 " b\n",
3053 )
3054 );
3055 }
3056
3057 #[test]
3067 fn numeric_source_recovery_follows_a_chained_call() {
3068 assert_eq!(
3069 parsed("x=a(0x10).b(16)"),
3070 concat!(
3071 "<desc_parameterlist multi_line_parameter_list=\"0\" multi_line_trailing_comma=\"1\" xml:space=\"preserve\">\n",
3072 " <desc_parameter xml:space=\"preserve\">\n",
3073 " <desc_sig_name classes=\"n\">\n",
3074 " x\n",
3075 " <desc_sig_operator classes=\"o\">\n",
3076 " =\n",
3077 " <inline classes=\"default_value\" support_smartquotes=\"0\">\n",
3078 " a(0x10).b(16)\n",
3079 )
3080 );
3081 }
3082
3083 #[test]
3088 fn numeric_source_recovery_follows_a_called_call() {
3089 assert_eq!(
3090 parsed("x=g(0xFF)(255)"),
3091 concat!(
3092 "<desc_parameterlist multi_line_parameter_list=\"0\" multi_line_trailing_comma=\"1\" xml:space=\"preserve\">\n",
3093 " <desc_parameter xml:space=\"preserve\">\n",
3094 " <desc_sig_name classes=\"n\">\n",
3095 " x\n",
3096 " <desc_sig_operator classes=\"o\">\n",
3097 " =\n",
3098 " <inline classes=\"default_value\" support_smartquotes=\"0\">\n",
3099 " g(0xFF)(255)\n",
3100 )
3101 );
3102 }
3103
3104 #[test]
3110 fn numeric_source_recovery_keeps_octal_spellings_in_a_chain() {
3111 assert_eq!(
3112 parsed("x=P(0o755).mask(0o022)"),
3113 concat!(
3114 "<desc_parameterlist multi_line_parameter_list=\"0\" multi_line_trailing_comma=\"1\" xml:space=\"preserve\">\n",
3115 " <desc_parameter xml:space=\"preserve\">\n",
3116 " <desc_sig_name classes=\"n\">\n",
3117 " x\n",
3118 " <desc_sig_operator classes=\"o\">\n",
3119 " =\n",
3120 " <inline classes=\"default_value\" support_smartquotes=\"0\">\n",
3121 " P(0o755).mask(0o022)\n",
3122 )
3123 );
3124 }
3125
3126 #[test]
3134 fn boolop_default_keeps_the_keyword_only_separator() {
3135 assert_eq!(
3136 parsed("a, *, x=A or B"),
3137 concat!(
3138 "<desc_parameterlist multi_line_parameter_list=\"0\" multi_line_trailing_comma=\"1\" xml:space=\"preserve\">\n",
3139 " <desc_parameter xml:space=\"preserve\">\n",
3140 " <desc_sig_name classes=\"n\">\n",
3141 " a\n",
3142 " <desc_parameter xml:space=\"preserve\">\n",
3143 " <desc_sig_operator classes=\"keyword-only-separator o\">\n",
3144 " <abbreviation explanation=\"Keyword-only parameters separator (PEP 3102)\">\n",
3145 " *\n",
3146 " <desc_parameter xml:space=\"preserve\">\n",
3147 " <desc_sig_name classes=\"n\">\n",
3148 " x\n",
3149 " <desc_sig_operator classes=\"o\">\n",
3150 " =\n",
3151 " <inline classes=\"default_value\" support_smartquotes=\"0\">\n",
3152 " A or B\n",
3153 )
3154 );
3155 }
3156
3157 #[test]
3162 fn boolop_default_keeps_the_positional_only_separator() {
3163 assert_eq!(
3164 parsed("a=A and B, /"),
3165 concat!(
3166 "<desc_parameterlist multi_line_parameter_list=\"0\" multi_line_trailing_comma=\"1\" xml:space=\"preserve\">\n",
3167 " <desc_parameter xml:space=\"preserve\">\n",
3168 " <desc_sig_name classes=\"n\">\n",
3169 " a\n",
3170 " <desc_sig_operator classes=\"o\">\n",
3171 " =\n",
3172 " <inline classes=\"default_value\" support_smartquotes=\"0\">\n",
3173 " A and B\n",
3174 " <desc_parameter xml:space=\"preserve\">\n",
3175 " <desc_sig_operator classes=\"positional-only-separator o\">\n",
3176 " <abbreviation explanation=\"Positional-only parameter separator (PEP 570)\">\n",
3177 " /\n",
3178 )
3179 );
3180 }
3181
3182 #[test]
3189 fn boolop_default_renders_without_parentheses() {
3190 assert_eq!(
3191 parsed("x=a and b or c"),
3192 concat!(
3193 "<desc_parameterlist multi_line_parameter_list=\"0\" multi_line_trailing_comma=\"1\" xml:space=\"preserve\">\n",
3194 " <desc_parameter xml:space=\"preserve\">\n",
3195 " <desc_sig_name classes=\"n\">\n",
3196 " x\n",
3197 " <desc_sig_operator classes=\"o\">\n",
3198 " =\n",
3199 " <inline classes=\"default_value\" support_smartquotes=\"0\">\n",
3200 " a and b or c\n",
3201 )
3202 );
3203 }
3204
3205 #[test]
3213 fn an_empty_type_parameter_bound_drops_the_parameter() {
3214 assert_eq!(
3215 tp("T:"),
3216 "<desc_type_parameter_list multi_line_parameter_list=\"0\" multi_line_trailing_comma=\"1\" xml:space=\"preserve\">\n",
3217 "empty bound in T:"
3218 );
3219 assert_eq!(
3220 tp("T: "),
3221 "<desc_type_parameter_list multi_line_parameter_list=\"0\" multi_line_trailing_comma=\"1\" xml:space=\"preserve\">\n",
3222 "empty bound in T: "
3223 );
3224 assert_eq!(
3225 tp("T:, U"),
3226 concat!(
3227 "<desc_type_parameter_list multi_line_parameter_list=\"0\" multi_line_trailing_comma=\"1\" xml:space=\"preserve\">\n",
3228 " <desc_type_parameter xml:space=\"preserve\">\n",
3229 " <desc_sig_name classes=\"n\">\n",
3230 " U\n",
3231 ),
3232 "empty bound in T:, U"
3233 );
3234 assert_eq!(
3235 tp("T, U:"),
3236 concat!(
3237 "<desc_type_parameter_list multi_line_parameter_list=\"0\" multi_line_trailing_comma=\"1\" xml:space=\"preserve\">\n",
3238 " <desc_type_parameter xml:space=\"preserve\">\n",
3239 " <desc_sig_name classes=\"n\">\n",
3240 " T\n",
3241 ),
3242 "empty bound in T, U:"
3243 );
3244 assert_eq!(
3245 tp("T: = int"),
3246 "<desc_type_parameter_list multi_line_parameter_list=\"0\" multi_line_trailing_comma=\"1\" xml:space=\"preserve\">\n",
3247 "empty bound in T: = int"
3248 );
3249 }
3250}