1use std::collections::{HashMap, HashSet};
28
29use crate::ast::{
30 AstNode, Command, Optional, child, children, command_name, control_word_range,
31 group_command_name, group_inner_source, nth_group, nth_group_inner, nth_group_text,
32};
33use crate::semantic::signature::{
34 ArgKind, ArgSpec, CommandSig, ContentKind, EnvironmentSig, SignatureDb, builtin,
35};
36use crate::semantic::xparse;
37use crate::syntax::{SyntaxKind, SyntaxNode, is_collapsible_trivia, is_trivia};
38use rowan::{NodeOrToken, TextRange, TextSize};
39use smol_str::SmolStr;
40
41pub fn scan_definitions(root: &SyntaxNode) -> SignatureDb {
46 let mut db = SignatureDb::default();
47 let mut bodies: HashMap<SmolStr, DefBody> = HashMap::new();
51 let mut env_bodies: HashMap<SmolStr, DefBody> = HashMap::new();
56 let mut alias_candidates: HashMap<SmolStr, EnvAliasCandidate> = HashMap::new();
61
62 for command in root
63 .descendants()
64 .filter(|node| node.kind() == SyntaxKind::COMMAND)
65 {
66 let Some(name) = command_name(&command) else {
67 continue;
68 };
69 match DefKind::of(&name) {
70 Some(DefKind::Command) => {
71 scan_newcommand(&command, &mut db, &mut bodies, &mut alias_candidates)
72 }
73 Some(DefKind::Def) => scan_def(&command, &mut db, &mut bodies, &mut alias_candidates),
74 Some(DefKind::Environment) => scan_newenvironment(&command, &mut db, &mut env_bodies),
75 Some(DefKind::XparseCommand) => {
76 scan_xparse_command(&command, &mut db, &mut bodies, &mut alias_candidates)
77 }
78 Some(DefKind::XparseEnvironment) => {
79 scan_xparse_environment(&command, &mut db, &mut env_bodies)
80 }
81 Some(DefKind::VerbatimEnvironment) => {
82 scan_verbatim_environment(&name, &command, &mut db)
83 }
84 None => {}
85 }
86 }
87
88 apply_verbatim_flags(&mut db, &bodies);
89 apply_verbatim_env_flags(&mut db, &env_bodies, &bodies);
90 apply_env_aliases(&mut db, &alias_candidates);
91 db
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96enum AliasSide {
97 Begin,
98 End,
99}
100
101#[derive(Debug, Clone)]
105struct EnvAliasCandidate {
106 side: AliasSide,
107 target: SmolStr,
108}
109
110fn env_alias_body(body: &SyntaxNode) -> Option<EnvAliasCandidate> {
122 let mut sole: Option<SyntaxNode> = None;
123 for child in body.children_with_tokens() {
124 match child {
125 NodeOrToken::Token(t) => match t.kind() {
126 SyntaxKind::L_BRACE | SyntaxKind::R_BRACE | SyntaxKind::COMMENT => {}
128 k if is_collapsible_trivia(k) => {}
129 _ => return None,
130 },
131 NodeOrToken::Node(n) => {
133 if sole.replace(n).is_some() {
134 return None;
135 }
136 }
137 }
138 }
139 let cmd = sole?;
140 if cmd.kind() != SyntaxKind::COMMAND {
141 return None;
142 }
143 if cmd.children().count() != 1 {
146 return None;
147 }
148 let side = match command_name(&cmd)?.as_str() {
149 "begin" => AliasSide::Begin,
150 "end" => AliasSide::End,
151 _ => return None,
152 };
153 let target = nth_group_text(&cmd, 0)?;
154 let target = target.trim();
155 if target.is_empty() {
156 return None;
157 }
158 Some(EnvAliasCandidate {
159 side,
160 target: SmolStr::new(target),
161 })
162}
163
164fn record_env_alias(
169 candidates: &mut HashMap<SmolStr, EnvAliasCandidate>,
170 name: &str,
171 arity: usize,
172 body: Option<&SyntaxNode>,
173) {
174 candidates.remove(name);
178 if arity != 0 {
179 return;
180 }
181 if let Some(candidate) = body.and_then(env_alias_body) {
182 candidates.insert(SmolStr::new(name), candidate);
183 }
184}
185
186fn apply_env_aliases(db: &mut SignatureDb, candidates: &HashMap<SmolStr, EnvAliasCandidate>) {
212 let admissible = |target: &str| {
213 builtin()
214 .environment(target)
215 .is_some_and(|sig| !sig.verbatim_body && sig.args.is_empty())
216 };
217 for (name, candidate) in candidates {
218 let target = candidate.target.as_str();
219 if !admissible(target) {
220 continue;
221 }
222 match candidate.side {
223 AliasSide::Begin => {
224 db.insert_env_begin_alias(name.clone(), candidate.target.clone());
225 }
226 AliasSide::End => {
227 db.insert_env_end_alias(name.clone(), candidate.target.clone());
228 }
229 }
230 }
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum DefSiteKind {
237 Command,
238 Environment,
239}
240
241#[derive(Debug, Clone, PartialEq, Eq)]
247pub struct DefSite {
248 pub name: SmolStr,
250 pub kind: DefSiteKind,
251 pub name_range: TextRange,
258 pub range: TextRange,
261}
262
263pub fn scan_definition_sites(root: &SyntaxNode) -> Vec<DefSite> {
269 let mut sites = Vec::new();
270 for command in root
271 .descendants()
272 .filter(|node| node.kind() == SyntaxKind::COMMAND)
273 {
274 let Some(name) = command_name(&command) else {
275 continue;
276 };
277 let site = match DefKind::of(&name) {
278 Some(DefKind::Command | DefKind::XparseCommand) => command_def_site(&command),
279 Some(DefKind::Def) => def_def_site(&command),
280 Some(
281 DefKind::Environment | DefKind::XparseEnvironment | DefKind::VerbatimEnvironment,
282 ) => environment_def_site(&command),
283 None => None,
284 };
285 sites.extend(site);
286 }
287 sites
288}
289
290fn command_def_site(command: &SyntaxNode) -> Option<DefSite> {
295 let def = resolve_command_def(command)?;
296 let name_range = if def.first_arg_group == 1 {
297 let group = nth_group(command, 0)?;
298 child::<Command>(&group)?.control_word_range()?
299 } else {
300 control_word_range(&def.host)?
301 };
302 Some(DefSite {
303 name: SmolStr::new(&def.name),
304 kind: DefSiteKind::Command,
305 name_range,
306 range: TextRange::new(
307 command.text_range().start(),
308 command.text_range().end().max(def.host.text_range().end()),
309 ),
310 })
311}
312
313fn def_def_site(command: &SyntaxNode) -> Option<DefSite> {
316 let name_node = adjacent_sibling_command(command)?;
317 let name = command_name(&name_node)?;
318 let name_range = control_word_range(&name_node)?;
319 Some(DefSite {
320 name: SmolStr::new(&name),
321 kind: DefSiteKind::Command,
322 name_range,
323 range: TextRange::new(command.text_range().start(), name_node.text_range().end()),
324 })
325}
326
327fn environment_def_site(command: &SyntaxNode) -> Option<DefSite> {
331 let (inner_range, text) = nth_group_inner(command, 0)?;
332 let trimmed = text.trim();
333 if trimmed.is_empty() {
334 return None;
335 }
336 let leading = text.len() - text.trim_start().len();
337 let name_range = TextRange::at(
338 inner_range.start() + TextSize::new(leading as u32),
339 TextSize::new(trimmed.len() as u32),
340 );
341 Some(DefSite {
342 name: SmolStr::new(trimmed),
343 kind: DefSiteKind::Environment,
344 name_range,
345 range: command.text_range(),
346 })
347}
348
349struct DefBody {
353 signal: bool,
356 called: Vec<SmolStr>,
359}
360
361fn apply_verbatim_flags(db: &mut SignatureDb, bodies: &HashMap<SmolStr, DefBody>) {
373 let verbatim: Vec<SmolStr> = bodies
374 .keys()
375 .filter(|name| {
376 db.command(name).is_some_and(|sig| !sig.args.is_empty())
378 && reaches_signal(name, bodies, &mut HashSet::new())
379 })
380 .cloned()
381 .collect();
382
383 for name in verbatim {
384 if let Some(mut sig) = db.command(&name).cloned() {
385 sig.args.to_mut().pop(); sig.verbatim = true;
387 db.insert_command(name, sig);
388 }
389 }
390}
391
392fn apply_verbatim_env_flags(
401 db: &mut SignatureDb,
402 env_bodies: &HashMap<SmolStr, DefBody>,
403 bodies: &HashMap<SmolStr, DefBody>,
404) {
405 let verbatim: Vec<SmolStr> = env_bodies
406 .iter()
407 .filter(|(name, body)| {
408 db.environment(name).is_some() && reaches_signal_body(body, bodies, &mut HashSet::new())
409 })
410 .map(|(name, _)| name.clone())
411 .collect();
412
413 for name in verbatim {
414 if let Some(mut sig) = db.environment(&name).cloned() {
415 sig.verbatim_body = true;
416 db.insert_environment(name, sig);
417 }
418 }
419}
420
421fn reaches_signal(
427 name: &str,
428 bodies: &HashMap<SmolStr, DefBody>,
429 visited: &mut HashSet<SmolStr>,
430) -> bool {
431 if !visited.insert(SmolStr::new(name)) {
432 return false;
433 }
434 let Some(body) = bodies.get(name) else {
435 return false;
436 };
437 reaches_signal_body(body, bodies, visited)
438}
439
440fn reaches_signal_body(
446 body: &DefBody,
447 bodies: &HashMap<SmolStr, DefBody>,
448 visited: &mut HashSet<SmolStr>,
449) -> bool {
450 body.signal
451 || body
452 .called
453 .iter()
454 .any(|callee| reaches_signal(callee, bodies, visited))
455}
456
457fn catcode_signal(body: &str) -> bool {
462 body.contains("\\@makeother")
463 || body.contains("\\@sanitize")
464 || body.contains("\\dospecials")
465 || body
466 .match_indices("\\catcode")
467 .any(|(start, _)| catcode_assigns_other(&body[start + "\\catcode".len()..]))
468}
469
470fn catcode_assigns_other(tail: &str) -> bool {
476 let mut chars = tail.chars();
477 if chars
478 .clone()
479 .next()
480 .is_some_and(|c| c.is_ascii_alphabetic() || matches!(c, '@' | '_' | ':'))
481 {
482 return false;
483 }
484
485 let mut after_equals = chars
486 .by_ref()
487 .take(64)
488 .skip_while(|&c| c != '=')
489 .skip(1)
490 .skip_while(|c| c.is_whitespace());
491 after_equals.next() == Some('1')
492 && after_equals.next() == Some('2')
493 && !after_equals.next().is_some_and(|c| c.is_ascii_digit())
494}
495
496fn called_macros(body: &str) -> Vec<SmolStr> {
501 body.match_indices('\\')
502 .filter_map(|(pos, _)| {
503 let after = &body[pos + 1..];
504 let len: usize = after
505 .chars()
506 .take_while(|c| c.is_ascii_alphabetic() || *c == '@')
507 .map(char::len_utf8)
508 .sum();
509 (len > 0).then(|| SmolStr::new(&after[..len]))
510 })
511 .collect()
512}
513
514const DELEGATING_PRIMITIVES: &[&str] = &["secdef", "@startsection", "@dblarg", "@sect", "@ssect"];
524
525fn keeps_builtin_over_arity0(name: &str, arity: usize, body: &DefBody) -> bool {
536 arity == 0
537 && body
538 .called
539 .iter()
540 .any(|callee| DELEGATING_PRIMITIVES.contains(&callee.as_str()))
541 && crate::semantic::signature::builtin()
542 .command(name)
543 .is_some()
544}
545
546pub fn is_definition_command(name: &str) -> bool {
553 DefKind::of(name).is_some()
554}
555
556enum DefKind {
558 Command,
559 Def,
560 Environment,
561 XparseCommand,
562 XparseEnvironment,
563 VerbatimEnvironment,
568}
569
570impl DefKind {
571 fn of(name: &str) -> Option<Self> {
572 Some(match name {
573 "newcommand" | "renewcommand" | "providecommand" | "DeclareRobustCommand" => {
574 DefKind::Command
575 }
576 "def" | "edef" | "gdef" | "xdef" => DefKind::Def,
579 "newenvironment" | "renewenvironment" => DefKind::Environment,
580 "NewDocumentCommand"
581 | "RenewDocumentCommand"
582 | "ProvideDocumentCommand"
583 | "DeclareDocumentCommand" => DefKind::XparseCommand,
584 "NewDocumentEnvironment"
585 | "RenewDocumentEnvironment"
586 | "ProvideDocumentEnvironment"
587 | "DeclareDocumentEnvironment" => DefKind::XparseEnvironment,
588 "lstnewenvironment" | "DefineVerbatimEnvironment" => DefKind::VerbatimEnvironment,
591 _ => return None,
592 })
593 }
594}
595
596fn scan_newcommand(
602 command: &SyntaxNode,
603 db: &mut SignatureDb,
604 bodies: &mut HashMap<SmolStr, DefBody>,
605 aliases: &mut HashMap<SmolStr, EnvAliasCandidate>,
606) {
607 let Some(def) = resolve_command_def(command) else {
608 return;
609 };
610 let (arity, first_optional) = newcommand_arity(&def.host);
611 let body = nth_group(&def.host, def.first_arg_group);
614 record_body(bodies, &def.name, body.as_ref());
615 record_env_alias(aliases, &def.name, arity, body.as_ref());
616 if bodies
620 .get(def.name.as_str())
621 .is_some_and(|body| keeps_builtin_over_arity0(&def.name, arity, body))
622 {
623 return;
624 }
625 db.insert_command(
626 def.name,
627 CommandSig {
628 args: latex2e_args(arity, first_optional).into(),
629 sectioning: None,
630 verbatim: false,
631 verbatim_delimited: false,
632 rule: false,
633 inline: false,
634 block: false,
638 },
639 );
640}
641
642fn scan_def(
650 command: &SyntaxNode,
651 db: &mut SignatureDb,
652 bodies: &mut HashMap<SmolStr, DefBody>,
653 aliases: &mut HashMap<SmolStr, EnvAliasCandidate>,
654) {
655 let Some(name_node) = adjacent_sibling_command(command) else {
656 return;
657 };
658 let Some(name) = command_name(&name_node) else {
659 return;
660 };
661 let (arity, body) = def_params_and_body(&name_node);
662 record_body(bodies, &name, body.as_ref());
663 record_env_alias(aliases, &name, arity, body.as_ref());
664 if bodies
667 .get(name.as_str())
668 .is_some_and(|body| keeps_builtin_over_arity0(&name, arity, body))
669 {
670 return;
671 }
672 db.insert_command(
673 name,
674 CommandSig {
675 args: latex2e_args(arity, false).into(),
679 sectioning: None,
680 verbatim: false,
681 verbatim_delimited: false,
682 rule: false,
683 inline: false,
684 block: false,
688 },
689 );
690}
691
692fn def_params_and_body(name_node: &SyntaxNode) -> (usize, Option<SyntaxNode>) {
705 if let Some(body) = nth_group(name_node, 0) {
707 return (0, Some(body));
708 }
709 let mut arity = 0usize;
711 let mut next = name_node.next_sibling_or_token();
712 while let Some(element) = next {
713 match element {
714 NodeOrToken::Token(token) if is_trivia(token.kind()) => {
715 next = token.next_sibling_or_token();
716 }
717 NodeOrToken::Token(token) if token.kind() == SyntaxKind::HASH => {
718 arity += 1;
719 next = token.next_sibling_or_token();
720 }
721 NodeOrToken::Token(token) if token.kind() == SyntaxKind::WORD => {
723 next = token.next_sibling_or_token();
724 }
725 NodeOrToken::Node(node) if node.kind() == SyntaxKind::GROUP => {
726 return (arity.min(9), Some(node));
727 }
728 _ => return (arity.min(9), None),
729 }
730 }
731 (arity.min(9), None)
732}
733
734fn record_body(bodies: &mut HashMap<SmolStr, DefBody>, name: &str, body: Option<&SyntaxNode>) {
737 let text = body.map(group_inner_source).unwrap_or_default();
738 bodies.insert(
739 SmolStr::new(name),
740 DefBody {
741 signal: catcode_signal(&text),
742 called: called_macros(&text),
743 },
744 );
745}
746
747fn scan_newenvironment(
752 command: &SyntaxNode,
753 db: &mut SignatureDb,
754 env_bodies: &mut HashMap<SmolStr, DefBody>,
755) {
756 let Some(name) = nth_group_text(command, 0) else {
757 return;
758 };
759 let name = name.trim();
760 if name.is_empty() {
761 return;
762 }
763 record_body(env_bodies, name, nth_group(command, 1).as_ref());
764 let (arity, first_optional) = newcommand_arity(command);
765 db.insert_environment(name, environment_sig(latex2e_args(arity, first_optional)));
766}
767
768fn scan_verbatim_environment(defining_command: &str, command: &SyntaxNode, db: &mut SignatureDb) {
778 let Some(name) = nth_group_text(command, 0) else {
779 return;
780 };
781 let name = name.trim();
782 if name.is_empty() {
783 return;
784 }
785 let args = if defining_command == "lstnewenvironment" {
786 let (arity, first_optional) = newcommand_arity(command);
787 latex2e_args(arity, first_optional)
788 } else {
789 latex2e_args(1, true)
791 };
792 let mut sig = environment_sig(args);
793 sig.verbatim_body = true;
794 db.insert_environment(name, sig);
795}
796
797fn scan_xparse_command(
802 command: &SyntaxNode,
803 db: &mut SignatureDb,
804 bodies: &mut HashMap<SmolStr, DefBody>,
805 aliases: &mut HashMap<SmolStr, EnvAliasCandidate>,
806) {
807 let Some(def) = resolve_command_def(command) else {
808 return;
809 };
810 let Some(spec) = nth_group(&def.host, def.first_arg_group) else {
811 return;
812 };
813 let body = nth_group(&def.host, def.first_arg_group + 1);
815 record_body(bodies, &def.name, body.as_ref());
816 let args = xparse::parse_spec(&group_inner_source(&spec));
817 record_env_alias(aliases, &def.name, args.len(), body.as_ref());
818 db.insert_command(
819 def.name,
820 CommandSig {
821 args: args.into(),
822 sectioning: None,
823 verbatim: false,
824 verbatim_delimited: false,
825 rule: false,
826 inline: false,
827 block: false,
831 },
832 );
833}
834
835struct CommandDef {
846 name: SmolStr,
847 host: SyntaxNode,
848 first_arg_group: usize,
849}
850
851fn resolve_command_def(command: &SyntaxNode) -> Option<CommandDef> {
855 if command.children().any(|c| c.kind() == SyntaxKind::GROUP) {
858 let name = nth_group(command, 0)
859 .as_ref()
860 .and_then(group_command_name)?;
861 return Some(CommandDef {
862 name,
863 host: command.clone(),
864 first_arg_group: 1,
865 });
866 }
867 let sibling = adjacent_sibling_command(command)?;
871 let name = command_name(&sibling)?;
872 Some(CommandDef {
873 name,
874 host: sibling,
875 first_arg_group: 0,
876 })
877}
878
879fn adjacent_sibling_command(command: &SyntaxNode) -> Option<SyntaxNode> {
885 let mut next = command.next_sibling_or_token();
886 while let Some(element) = next {
887 match element {
888 NodeOrToken::Token(token) if is_trivia(token.kind()) => {
889 next = token.next_sibling_or_token();
890 }
891 NodeOrToken::Node(node) if node.kind() == SyntaxKind::COMMAND => return Some(node),
892 _ => return None,
893 }
894 }
895 None
896}
897
898fn scan_xparse_environment(
902 command: &SyntaxNode,
903 db: &mut SignatureDb,
904 env_bodies: &mut HashMap<SmolStr, DefBody>,
905) {
906 let Some(name) = nth_group_text(command, 0) else {
907 return;
908 };
909 let name = name.trim();
910 if name.is_empty() {
911 return;
912 }
913 let Some(spec) = nth_group(command, 1) else {
914 return;
915 };
916 record_body(env_bodies, name, nth_group(command, 2).as_ref());
917 db.insert_environment(
918 name,
919 environment_sig(xparse::parse_spec(&group_inner_source(&spec))),
920 );
921}
922
923fn newcommand_arity(command: &SyntaxNode) -> (usize, bool) {
927 let optionals: Vec<Optional> = children::<Optional>(command).collect();
928 let arity = optionals
929 .first()
930 .map(|o| o.syntax())
931 .and_then(optional_number)
932 .unwrap_or(0)
933 .min(9); (arity, optionals.len() >= 2)
935}
936
937fn optional_number(node: &SyntaxNode) -> Option<usize> {
940 let text = node.text().to_string();
941 let inner = text.strip_prefix('[').unwrap_or(&text);
942 let inner = inner.strip_suffix(']').unwrap_or(inner);
943 inner.trim().parse().ok()
944}
945
946fn latex2e_args(arity: usize, first_optional: bool) -> Vec<ArgSpec> {
949 (0..arity)
950 .map(|i| {
951 if i == 0 && first_optional {
952 ArgSpec {
953 required: false,
954 kind: ArgKind::Bracket,
955 content: ContentKind::Opaque,
956 domain: crate::semantic::ArgumentDomain::Unknown,
957 verbatim: false,
958 }
959 } else {
960 ArgSpec {
961 required: true,
962 kind: ArgKind::Brace,
963 content: ContentKind::Opaque,
964 domain: crate::semantic::ArgumentDomain::Unknown,
965 verbatim: false,
966 }
967 }
968 })
969 .collect()
970}
971
972fn environment_sig(args: Vec<ArgSpec>) -> EnvironmentSig {
976 EnvironmentSig {
977 args: args.into(),
978 verbatim_body: false,
979 verbatim_arg: false,
982 math: false,
983 code: false,
984 statement_body: false,
987 label_key: false,
989 caption_container: false,
991 align: false,
992 no_indent: false,
993 list: false,
996 block_explicit: false,
1000 outline: None,
1003 }
1004}
1005
1006#[cfg(test)]
1007mod tests {
1008 use super::*;
1009 use crate::parser::{parse, reconstruct};
1010
1011 fn db_of(src: &str) -> SignatureDb {
1012 assert_eq!(reconstruct(src), src, "reconstruct must round-trip");
1013 scan_definitions(&SyntaxNode::new_root(parse(src).green))
1014 }
1015
1016 fn arg_kinds(args: &[ArgSpec]) -> Vec<ArgKind> {
1017 args.iter().map(|a| a.kind).collect()
1018 }
1019
1020 #[test]
1021 fn newcommand_counts_mandatory_args() {
1022 let db = db_of("\\newcommand{\\foo}[2]{#1#2}\n");
1023 let sig = db.command("foo").expect("foo defined");
1024 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace, ArgKind::Brace]);
1025 assert!(sig.args.iter().all(|a| a.required));
1026 assert!(
1027 sig.args
1028 .iter()
1029 .all(|arg| arg.domain == crate::semantic::ArgumentDomain::Unknown)
1030 );
1031 }
1032
1033 #[test]
1034 fn newcommand_optional_first_arg() {
1035 let db = db_of("\\newcommand{\\foo}[2][d]{#1#2}\n");
1036 let sig = db.command("foo").expect("foo defined");
1037 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket, ArgKind::Brace]);
1038 assert!(!sig.args[0].required);
1039 assert!(sig.args[1].required);
1040 }
1041
1042 #[test]
1043 fn newcommand_zero_args() {
1044 let db = db_of("\\newcommand{\\foo}{bar}\n");
1045 assert!(db.command("foo").expect("foo defined").args.is_empty());
1046 }
1047
1048 #[test]
1049 fn renew_and_provide_recognized() {
1050 let db = db_of("\\renewcommand{\\a}[1]{x}\\providecommand{\\b}[1]{y}\n");
1051 assert_eq!(db.command("a").unwrap().args.len(), 1);
1052 assert_eq!(db.command("b").unwrap().args.len(), 1);
1053 }
1054
1055 #[test]
1056 fn secdef_redefinition_keeps_builtin_prose() {
1057 let db = db_of("\\renewcommand{\\section}{\\secdef \\a \\b}\n");
1058 assert!(
1059 db.command("section").is_none(),
1060 "the delegating redefinition must not be recorded as a scanned override"
1061 );
1062 let sigs = crate::semantic::signature::Signatures::new(&db);
1063 let sig = sigs.command("section").expect("built-in section survives");
1064 let last = sig.args.last().expect("section keeps its title argument");
1065 assert_eq!(
1066 last.content,
1067 crate::semantic::signature::ContentKind::Prose,
1068 "the title argument stays prose (reflowable)"
1069 );
1070 }
1071
1072 #[test]
1073 fn genuine_zero_arg_redefinition_downgrades_builtin() {
1074 let db = db_of("\\renewcommand{\\section}{\\textbf{Fixed}}\n");
1075 let sig = db
1076 .command("section")
1077 .expect("genuine 0-arg redefinition is recorded");
1078 assert!(
1079 sig.args.is_empty(),
1080 "no delegation means the scanned 0-arg signature wins"
1081 );
1082 }
1083
1084 #[test]
1085 fn secdef_redefinition_of_unknown_still_records() {
1086 let db = db_of("\\renewcommand{\\mysec}{\\secdef \\a \\b}\n");
1087 let sig = db.command("mysec").expect("unknown name is still recorded");
1088 assert!(sig.args.is_empty());
1089 }
1090
1091 #[test]
1092 fn newenvironment_args() {
1093 let db = db_of("\\newenvironment{thm}[1]{begin #1}{end}\n");
1094 let sig = db.environment("thm").expect("thm defined");
1095 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace]);
1096 assert!(sig.reflow());
1097 assert!(!sig.verbatim_body);
1098 assert!(!sig.math);
1099 }
1100
1101 #[test]
1102 fn xparse_command_spec() {
1103 let db = db_of("\\NewDocumentCommand{\\foo}{m O{d} m}{x}\n");
1104 let sig = db.command("foo").expect("foo defined");
1105 assert_eq!(
1106 arg_kinds(&sig.args),
1107 vec![ArgKind::Brace, ArgKind::Bracket, ArgKind::Brace]
1108 );
1109 assert!(
1110 sig.args
1111 .iter()
1112 .all(|arg| arg.domain == crate::semantic::ArgumentDomain::Unknown)
1113 );
1114 }
1115
1116 #[test]
1117 fn xparse_environment_spec() {
1118 let db = db_of("\\NewDocumentEnvironment{env}{O{x} m}{a}{b}\n");
1119 let sig = db.environment("env").expect("env defined");
1120 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket, ArgKind::Brace]);
1121 assert!(
1122 sig.args
1123 .iter()
1124 .all(|arg| arg.domain == crate::semantic::ArgumentDomain::Unknown)
1125 );
1126 }
1127
1128 #[test]
1129 fn unbraced_newcommand_extracted() {
1130 let db = db_of("\\newcommand\\foo[2]{#1#2}\n");
1131 let sig = db.command("foo").expect("foo defined");
1132 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace, ArgKind::Brace]);
1133 assert!(sig.args.iter().all(|a| a.required));
1134 }
1135
1136 #[test]
1137 fn unbraced_optional_first_arg() {
1138 let db = db_of("\\newcommand\\foo[2][d]{#1#2}\n");
1139 let sig = db.command("foo").expect("foo defined");
1140 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket, ArgKind::Brace]);
1141 assert!(!sig.args[0].required);
1142 assert!(sig.args[1].required);
1143 }
1144
1145 #[test]
1146 fn unbraced_zero_args() {
1147 let db = db_of("\\newcommand\\foo{x}\n");
1148 assert!(db.command("foo").expect("foo defined").args.is_empty());
1149 }
1150
1151 #[test]
1152 fn unbraced_spaced_binds() {
1153 let db = db_of("\\newcommand \\foo[1]{x}\n");
1154 assert_eq!(db.command("foo").unwrap().args.len(), 1);
1155 }
1156
1157 #[test]
1158 fn unbraced_renewcommand() {
1159 let db = db_of("\\renewcommand\\foo[1]{x}\n");
1160 assert_eq!(db.command("foo").unwrap().args.len(), 1);
1161 }
1162
1163 #[test]
1164 fn unbraced_xparse_command() {
1165 let db = db_of("\\NewDocumentCommand\\foo{m O{d} m}{x}\n");
1166 let sig = db.command("foo").expect("foo defined");
1167 assert_eq!(
1168 arg_kinds(&sig.args),
1169 vec![ArgKind::Brace, ArgKind::Bracket, ArgKind::Brace]
1170 );
1171 }
1172
1173 #[test]
1174 fn unbraced_stray_text_not_bound() {
1175 let db = db_of("\\newcommand foo \\bar{x}\n");
1176 assert!(db.command("foo").is_none());
1177 assert!(db.command("bar").is_none());
1178 }
1179
1180 #[test]
1181 fn redefinition_last_wins() {
1182 let db = db_of("\\newcommand{\\foo}[1]{x}\\renewcommand{\\foo}[3]{y}\n");
1183 assert_eq!(db.command("foo").unwrap().args.len(), 3);
1184 }
1185
1186 #[test]
1187 fn garbage_definition_degrades_to_no_insert() {
1188 let db = db_of("\\newcommand\n");
1189 assert!(db.command("foo").is_none());
1190 }
1191
1192 #[test]
1193 fn nested_definition_collected() {
1194 let db = db_of("\\begin{document}\n\\newcommand{\\foo}[1]{x}\n\\end{document}\n");
1195 assert_eq!(db.command("foo").unwrap().args.len(), 1);
1196 }
1197
1198 #[test]
1199 fn commented_definition_ignored() {
1200 let db = db_of("% \\newcommand{\\foo}[1]{x}\n");
1201 assert!(db.command("foo").is_none());
1202 }
1203
1204 #[test]
1205 fn verbatim_makeother_flagged() {
1206 let db = db_of("\\newcommand\\shellcmd[1]{\\@makeother\\$#1}\n");
1207 let sig = db.command("shellcmd").expect("shellcmd defined");
1208 assert!(sig.verbatim);
1209 assert!(sig.args.is_empty());
1210 }
1211
1212 #[test]
1213 fn verbatim_catcode_flagged() {
1214 let db = db_of("\\newcommand\\shellcmd[1]{\\catcode 36=12 #1}\n");
1215 assert!(db.command("shellcmd").expect("shellcmd defined").verbatim);
1216 }
1217
1218 #[test]
1219 fn unrelated_twelve_does_not_flag_catcode_assignment() {
1220 let db = db_of("\\newcommand\\ordinary[1]{\\catcode 36=\\active \\hspace{12pt}#1}\n");
1221 assert!(!db.command("ordinary").expect("ordinary defined").verbatim);
1222 }
1223
1224 #[test]
1225 fn verbatim_dospecials_flagged() {
1226 let db = db_of("\\newcommand\\shellcmd[1]{\\let\\do\\@makeother\\dospecials #1}\n");
1227 assert!(db.command("shellcmd").expect("shellcmd defined").verbatim);
1228 }
1229
1230 #[test]
1231 fn verbatim_keeps_leading_args() {
1232 let db = db_of("\\newcommand\\mycode[2]{\\@makeother\\$#1#2}\n");
1233 let sig = db.command("mycode").expect("mycode defined");
1234 assert!(sig.verbatim);
1235 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace]);
1236 }
1237
1238 #[test]
1239 fn verbatim_via_chained_helper() {
1240 let db =
1241 db_of("\\newcommand\\setup{\\@makeother\\$}\\newcommand\\shellcmd[1]{\\setup#1}\n");
1242 assert!(db.command("shellcmd").expect("shellcmd defined").verbatim);
1243 assert!(!db.command("setup").expect("setup defined").verbatim);
1244 }
1245
1246 #[test]
1247 fn verbatim_chain_cycle_terminates() {
1248 let db = db_of("\\newcommand\\a[1]{\\b#1}\\newcommand\\b[1]{\\a#1}\n");
1249 assert!(!db.command("a").expect("a defined").verbatim);
1250 assert!(!db.command("b").expect("b defined").verbatim);
1251 }
1252
1253 #[test]
1254 fn ordinary_command_not_verbatim() {
1255 let db = db_of("\\newcommand\\foo[1]{\\emph{#1}}\n");
1256 assert!(!db.command("foo").expect("foo defined").verbatim);
1257 }
1258
1259 #[test]
1260 fn verbatim_needs_an_argument() {
1261 let db = db_of("\\newcommand\\setup{\\@makeother\\$}\n");
1262 assert!(!db.command("setup").expect("setup defined").verbatim);
1263 }
1264
1265 #[test]
1266 fn def_helper_chain_followed() {
1267 let db = db_of("\\def\\setup{\\@makeother\\$}\\newcommand\\shellcmd[1]{\\setup#1}\n");
1268 assert!(db.command("shellcmd").expect("shellcmd defined").verbatim);
1269 assert!(!db.command("setup").expect("setup defined").verbatim);
1270 }
1271
1272 #[test]
1273 fn def_direct_verbatim_flagged() {
1274 let db = db_of("\\def\\shellcmd#1{\\@makeother\\$#1}\n");
1275 let sig = db.command("shellcmd").expect("shellcmd defined");
1276 assert!(sig.verbatim);
1277 assert!(sig.args.is_empty());
1278 }
1279
1280 #[test]
1281 fn def_zero_params() {
1282 let db = db_of("\\def\\foo{x}\n");
1283 let sig = db.command("foo").expect("foo defined");
1284 assert!(sig.args.is_empty());
1285 assert!(!sig.verbatim);
1286 }
1287
1288 #[test]
1289 fn def_counts_params() {
1290 let db = db_of("\\def\\foo#1#2{#1#2}\n");
1291 let sig = db.command("foo").expect("foo defined");
1292 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace, ArgKind::Brace]);
1293 }
1294
1295 #[test]
1296 fn def_variants_scanned() {
1297 let db = db_of("\\edef\\a#1{x}\\gdef\\b{y}\\xdef\\c#1{\\@makeother\\$#1}\n");
1298 assert_eq!(db.command("a").expect("a defined").args.len(), 1);
1299 assert!(db.command("b").expect("b defined").args.is_empty());
1300 let c = db.command("c").expect("c defined");
1301 assert!(c.verbatim);
1302 assert!(c.args.is_empty());
1303 }
1304
1305 #[test]
1306 fn def_chain_through_def_helpers() {
1307 let db = db_of(
1308 "\\def\\inner{\\@makeother\\$}\\def\\outer{\\inner}\\newcommand\\cmd[1]{\\outer#1}\n",
1309 );
1310 assert!(db.command("cmd").expect("cmd defined").verbatim);
1311 }
1312
1313 #[test]
1314 fn verbatim_xparse_flagged() {
1315 let db = db_of("\\NewDocumentCommand\\shellcmd{m}{\\@makeother\\$#1}\n");
1316 let sig = db.command("shellcmd").expect("shellcmd defined");
1317 assert!(sig.verbatim);
1318 assert!(sig.args.is_empty());
1319 }
1320
1321 #[test]
1322 fn env_makeother_flagged() {
1323 let db = db_of("\\newenvironment{shellenv}{\\@makeother\\$}{}\n");
1324 let sig = db.environment("shellenv").expect("shellenv defined");
1325 assert!(sig.verbatim_body);
1326 assert!(!sig.reflow()); }
1328
1329 #[test]
1330 fn env_catcode_flagged() {
1331 let db = db_of("\\newenvironment{shellenv}[1]{\\catcode 36=12 }{}\n");
1332 let sig = db.environment("shellenv").expect("shellenv defined");
1333 assert!(sig.verbatim_body);
1334 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace]);
1335 }
1336
1337 #[test]
1338 fn env_via_chained_helper() {
1339 let db =
1340 db_of("\\newcommand\\setup{\\@makeother\\$}\\newenvironment{shellenv}{\\setup}{}\n");
1341 assert!(
1342 db.environment("shellenv")
1343 .expect("shellenv defined")
1344 .verbatim_body
1345 );
1346 }
1347
1348 #[test]
1349 fn env_without_signal_not_flagged() {
1350 let db = db_of("\\newenvironment{remark}{\\par\\noindent\\textbf{Remark.}}{\\par}\n");
1351 let sig = db.environment("remark").expect("remark defined");
1352 assert!(!sig.verbatim_body);
1353 assert!(sig.reflow());
1354 }
1355
1356 #[test]
1357 fn lstnewenvironment_flagged_verbatim() {
1358 let db = db_of("\\lstnewenvironment{demo}[1][code]{\\lstset{#1}}{}\n");
1359 let sig = db.environment("demo").expect("demo defined");
1360 assert!(sig.verbatim_body);
1361 assert!(!sig.reflow());
1362 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket]);
1363 }
1364
1365 #[test]
1366 fn lstnewenvironment_no_args_flagged_verbatim() {
1367 let db = db_of("\\lstnewenvironment{demo}{}{}\n");
1368 let sig = db.environment("demo").expect("demo defined");
1369 assert!(sig.verbatim_body);
1370 assert!(sig.args.is_empty());
1371 }
1372
1373 #[test]
1374 fn defineverbatimenvironment_flagged_verbatim() {
1375 let db = db_of("\\DefineVerbatimEnvironment{code}{Verbatim}{fontsize=\\small}\n");
1376 let sig = db.environment("code").expect("code defined");
1377 assert!(sig.verbatim_body);
1378 assert!(!sig.reflow());
1379 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket]);
1380 }
1381
1382 #[test]
1383 fn xparse_env_makeother_flagged() {
1384 let db = db_of("\\NewDocumentEnvironment{shellenv}{O{x}}{\\dospecials}{}\n");
1385 let sig = db.environment("shellenv").expect("shellenv defined");
1386 assert!(sig.verbatim_body);
1387 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket]);
1388 }
1389
1390 fn sites_of(src: &str) -> Vec<DefSite> {
1391 assert_eq!(reconstruct(src), src, "reconstruct must round-trip");
1392 scan_definition_sites(&SyntaxNode::new_root(parse(src).green))
1393 }
1394
1395 #[test]
1396 fn def_site_newcommand_braced_name_span() {
1397 let src = "\\newcommand{\\foo}[1]{#1}\n";
1398 let sites = sites_of(src);
1399 assert_eq!(sites.len(), 1);
1400 let site = &sites[0];
1401 assert_eq!(site.name, "foo");
1402 assert_eq!(site.kind, DefSiteKind::Command);
1403 assert_eq!(&src[site.name_range], "\\foo");
1404 assert_eq!(&src[site.range], "\\newcommand{\\foo}[1]{#1}");
1405 }
1406
1407 #[test]
1408 fn def_site_newcommand_unbraced_name_span() {
1409 let src = "\\newcommand\\foo[1]{#1}\n";
1410 let sites = sites_of(src);
1411 assert_eq!(sites.len(), 1);
1412 assert_eq!(sites[0].name, "foo");
1413 assert_eq!(&src[sites[0].name_range], "\\foo");
1414 assert_eq!(&src[sites[0].range], "\\newcommand\\foo[1]{#1}");
1415 }
1416
1417 #[test]
1418 fn def_site_def_sibling_name_span() {
1419 let src = "\\def\\foo#1{#1}\n";
1420 let sites = sites_of(src);
1421 assert_eq!(sites.len(), 1);
1422 assert_eq!(sites[0].name, "foo");
1423 assert_eq!(sites[0].kind, DefSiteKind::Command);
1424 assert_eq!(&src[sites[0].name_range], "\\foo");
1425 }
1426
1427 #[test]
1428 fn def_site_xparse_command_name_span() {
1429 let src = "\\NewDocumentCommand{\\foo}{m O{d}}{x}\n";
1430 let sites = sites_of(src);
1431 assert_eq!(sites.len(), 1);
1432 assert_eq!(sites[0].name, "foo");
1433 assert_eq!(&src[sites[0].name_range], "\\foo");
1434 }
1435
1436 #[test]
1437 fn def_site_newenvironment_name_span() {
1438 let src = "\\newenvironment{myenv}{begin}{end}\n";
1439 let sites = sites_of(src);
1440 assert_eq!(sites.len(), 1);
1441 let site = &sites[0];
1442 assert_eq!(site.name, "myenv");
1443 assert_eq!(site.kind, DefSiteKind::Environment);
1444 assert_eq!(&src[site.name_range], "myenv");
1445 }
1446
1447 #[test]
1448 fn def_site_xparse_environment_name_span() {
1449 let src = "\\NewDocumentEnvironment{myenv}{m}{a}{b}\n";
1450 let sites = sites_of(src);
1451 assert_eq!(sites.len(), 1);
1452 assert_eq!(sites[0].name, "myenv");
1453 assert_eq!(sites[0].kind, DefSiteKind::Environment);
1454 assert_eq!(&src[sites[0].name_range], "myenv");
1455 }
1456
1457 #[test]
1458 fn def_site_keeps_every_redefinition() {
1459 let src = "\\newcommand{\\foo}{a}\n\\renewcommand{\\foo}{b}\n";
1460 let sites = sites_of(src);
1461 assert_eq!(sites.len(), 2);
1462 assert!(sites.iter().all(|s| s.name == "foo"));
1463 assert!(sites[0].name_range.start() < sites[1].name_range.start());
1464 }
1465
1466 #[test]
1467 fn def_site_none_for_malformed() {
1468 assert!(sites_of("\\newcommand\n").is_empty());
1469 assert!(sites_of("\\newenvironment{}{a}{b}\n").is_empty());
1470 }
1471
1472 const EQNARRAY_PAIR: &str =
1473 "\\newcommand{\\bea}{\\begin{eqnarray}}\n\\newcommand{\\eea}{\\end{eqnarray}}\n";
1474
1475 #[test]
1476 fn newcommand_env_alias_pair_is_recorded() {
1477 let db = db_of(EQNARRAY_PAIR);
1478 assert_eq!(db.env_begin_alias("bea"), Some("eqnarray"));
1479 assert_eq!(db.env_end_alias("eea"), Some("eqnarray"));
1480 assert_eq!(db.env_begin_alias("eea"), None);
1481 assert_eq!(db.env_end_alias("bea"), None);
1482 }
1483
1484 #[test]
1485 fn def_env_alias_pair_is_recorded() {
1486 let db = db_of("\\def\\bea{\\begin{eqnarray}}\n\\def\\eea{\\end{eqnarray}}\n");
1487 assert_eq!(db.env_begin_alias("bea"), Some("eqnarray"));
1488 assert_eq!(db.env_end_alias("eea"), Some("eqnarray"));
1489 }
1490
1491 #[test]
1492 fn xparse_env_alias_pair_is_recorded() {
1493 let db = db_of(
1494 "\\NewDocumentCommand{\\bea}{}{\\begin{eqnarray}}\n\
1495 \\NewDocumentCommand{\\eea}{}{\\end{eqnarray}}\n",
1496 );
1497 assert_eq!(db.env_begin_alias("bea"), Some("eqnarray"));
1498 assert_eq!(db.env_end_alias("eea"), Some("eqnarray"));
1499 }
1500
1501 #[test]
1502 fn env_alias_body_tolerates_trivia() {
1503 let db = db_of(
1504 "\\newcommand{\\bea}{ \\begin{eqnarray} }\n\\newcommand{\\eea}{% why\n\\end{eqnarray}}\n",
1505 );
1506 assert_eq!(db.env_begin_alias("bea"), Some("eqnarray"));
1507 assert_eq!(db.env_end_alias("eea"), Some("eqnarray"));
1508 }
1509
1510 #[test]
1511 fn a_lone_env_alias_half_is_recorded() {
1512 let db = db_of("\\newcommand{\\bea}{\\begin{eqnarray}}\n");
1513 assert_eq!(db.env_begin_alias("bea"), Some("eqnarray"));
1514 assert_eq!(db.env_end_alias("bea"), None);
1515
1516 let db = db_of("\\newcommand{\\eea}{\\end{eqnarray}}\n");
1517 assert_eq!(db.env_end_alias("eea"), Some("eqnarray"));
1518 assert_eq!(db.env_begin_alias("eea"), None);
1519 }
1520
1521 #[test]
1522 fn a_lone_half_still_obeys_every_target_rule() {
1523 assert_eq!(
1524 db_of("\\newcommand{\\bmy}{\\begin{notacuratedenv}}\n").env_begin_alias("bmy"),
1525 None
1526 );
1527 assert_eq!(
1528 db_of("\\newcommand{\\ev}{\\end{verbatim}}\n").env_end_alias("ev"),
1529 None
1530 );
1531 assert_eq!(
1532 db_of("\\newcommand{\\bt}{\\begin{tabular}}\n").env_begin_alias("bt"),
1533 None
1534 );
1535 assert_eq!(
1536 db_of("\\newcommand{\\bea}[1]{\\begin{eqnarray}}\n").env_begin_alias("bea"),
1537 None
1538 );
1539 }
1540
1541 #[test]
1542 fn env_alias_rejects_uncurated_target() {
1543 let db = db_of(
1544 "\\newcommand{\\bmy}{\\begin{notacuratedenv}}\n\
1545 \\newcommand{\\emy}{\\end{notacuratedenv}}\n",
1546 );
1547 assert_eq!(db.env_begin_alias("bmy"), None);
1548 assert_eq!(db.env_end_alias("emy"), None);
1549 }
1550
1551 #[test]
1552 fn env_alias_rejects_verbatim_target() {
1553 let db =
1554 db_of("\\newcommand{\\bv}{\\begin{verbatim}}\n\\newcommand{\\ev}{\\end{verbatim}}\n");
1555 assert_eq!(db.env_begin_alias("bv"), None);
1556 }
1557
1558 #[test]
1559 fn env_alias_rejects_argument_taking_target() {
1560 let db =
1561 db_of("\\newcommand{\\bt}{\\begin{tabular}}\n\\newcommand{\\et}{\\end{tabular}}\n");
1562 assert_eq!(db.env_begin_alias("bt"), None);
1563 }
1564
1565 #[test]
1566 fn env_alias_rejects_parameterized_definition() {
1567 let db = db_of(
1568 "\\newcommand{\\bt}[1]{\\begin{tabular}{#1}}\n\\newcommand{\\et}{\\end{tabular}}\n",
1569 );
1570 assert_eq!(db.env_begin_alias("bt"), None);
1571 }
1572
1573 #[test]
1574 fn env_alias_rejects_body_with_extra_content() {
1575 let db = db_of(
1576 "\\newcommand{\\bea}{\\begin{eqnarray}\\label{x}}\n\\newcommand{\\eea}{\\end{eqnarray}}\n",
1577 );
1578 assert_eq!(db.env_begin_alias("bea"), None);
1579 }
1580
1581 #[test]
1582 fn redefinition_retracts_an_earlier_env_alias() {
1583 let db = db_of(
1584 "\\newcommand{\\bea}{\\begin{eqnarray}}\n\
1585 \\newcommand{\\eea}{\\end{eqnarray}}\n\
1586 \\renewcommand{\\bea}{\\textbf}\n",
1587 );
1588 assert_eq!(db.env_begin_alias("bea"), None);
1589 }
1590
1591 #[test]
1592 fn env_alias_does_not_pollute_the_environment_namespace() {
1593 let db = db_of(EQNARRAY_PAIR);
1594 assert!(db.environment("bea").is_none());
1595 assert!(!db.environment_names().any(|n| n == "bea"));
1596 }
1597
1598 fn environment_node(src: &str) -> SyntaxNode {
1599 SyntaxNode::new_root(parse(src).green)
1600 .descendants()
1601 .find(|n| n.kind() == SyntaxKind::ENVIRONMENT)
1602 .expect("an environment")
1603 }
1604
1605 #[test]
1606 fn signatures_resolves_an_env_alias_to_the_curated_target() {
1607 use crate::semantic::signature::Signatures;
1608 let src = format!("{EQNARRAY_PAIR}\\bea a \\eea\n");
1609 let db = db_of(&src);
1610 let sigs = Signatures::new(&db);
1611 let sig = sigs
1612 .environment_at(&environment_node(&src))
1613 .expect("alias resolves");
1614 let target = builtin().environment("eqnarray").expect("curated target");
1615 assert_eq!(sig, target);
1616 assert!(sig.math && sig.align);
1617 assert!(sigs.environment("bea").is_none());
1618 }
1619
1620 #[test]
1621 fn a_literal_begin_does_not_inherit_the_alias_target() {
1622 use crate::semantic::signature::Signatures;
1623 let src = format!("{EQNARRAY_PAIR}\\begin{{bea}} a \\end{{bea}}\n");
1624 let db = db_of(&src);
1625 let sigs = Signatures::new(&db);
1626 assert!(sigs.environment_at(&environment_node(&src)).is_none());
1627 }
1628
1629 #[test]
1630 fn a_real_environment_wins_over_an_alias_of_the_same_name() {
1631 use crate::semantic::signature::Signatures;
1632 let defs = "\\newcommand{\\bea}{\\begin{eqnarray}}\n\
1633 \\newcommand{\\eea}{\\end{eqnarray}}\n\
1634 \\newenvironment{bea}{x}{y}\n";
1635 let db = db_of(&format!("{defs}\\begin{{bea}} a \\end{{bea}}\n"));
1636 let sigs = Signatures::new(&db);
1637 let sig = sigs
1638 .environment_at(&environment_node(&format!(
1639 "{defs}\\begin{{bea}} a \\end{{bea}}\n"
1640 )))
1641 .expect("real environment resolves");
1642 assert!(
1643 !sig.math,
1644 "the scanned \\newenvironment must win for a literal `\\begin{{bea}}`"
1645 );
1646 assert!(
1647 sigs.environment_at(&environment_node(&format!("{defs}\\bea a \\eea\n")))
1648 .is_some_and(|sig| sig.math),
1649 "while the alias delimiters still resolve to eqnarray"
1650 );
1651 }
1652
1653 #[test]
1654 fn merge_from_carries_env_aliases() {
1655 let mut target = SignatureDb::default();
1656 target.merge_from(&db_of(EQNARRAY_PAIR), None);
1657 assert_eq!(target.env_begin_alias("bea"), Some("eqnarray"));
1658 assert_eq!(target.env_end_alias("eea"), Some("eqnarray"));
1659 }
1660}