1use std::{
2 collections::{HashMap, HashSet},
3 ops::{Deref, DerefMut, RangeInclusive},
4 path::{Path, PathBuf},
5};
6
7use crate::{
8 datatype::{Datatype, StaticLookup, evaluate_construct, shorthand_rebind},
9 lexer::Token,
10 parser::{AstErrors, Construct, Delimited, Node, ParsedRsml},
11 range_from_span::RangeFromSpan,
12 types::{Diagnostic, Range},
13};
14
15use self::luaurc::Luaurc;
16use crate::types::LanguageMode;
17pub use macro_check::{
18 MacroDefinition, MacroKey, MacroRegistry, MacroReturnContext, collect_macro_def_arg_names,
19 macro_return_context,
20};
21
22use rangemap::RangeInclusiveMap;
23
24mod annotations;
25mod derive;
26pub mod luaurc;
27mod macro_check;
28pub(crate) mod multibimap;
29pub(crate) mod normalize_path;
30mod properties;
31mod selectors;
32mod tween;
33mod type_error;
34
35pub use type_error::*;
36
37pub trait ReportTypeError {
38 fn report(&mut self, error: TypeError, range: Range);
39}
40
41impl ReportTypeError for AstErrors {
42 fn report(&mut self, error: TypeError, range: Range) {
43 self.0.push(Diagnostic {
44 range,
45 severity: error.severity(),
46 code: error.to_string(),
47 message: error.message(),
48 data: error.data(),
49 });
50 }
51}
52
53pub struct Definitions(RangeInclusiveMap<usize, DefinitionKind>);
54
55impl Definitions {
56 pub fn new() -> Self {
57 Self(RangeInclusiveMap::new())
58 }
59}
60
61impl Deref for Definitions {
62 type Target = RangeInclusiveMap<usize, DefinitionKind>;
63
64 fn deref(&self) -> &Self::Target {
65 &self.0
66 }
67}
68
69impl DerefMut for Definitions {
70 fn deref_mut(&mut self) -> &mut Self::Target {
71 &mut self.0
72 }
73}
74
75#[derive(PartialEq, Eq, Clone)]
76pub enum DefinitionKind {
77 Derive {
78 path: PathBuf,
79 },
80 Selector {
81 type_definition: Vec<String>,
82 hint: String,
83 },
84 Scope {
85 type_definition: Vec<String>,
86 },
87 Assignment {
88 property_name: String,
89 type_definition: Vec<String>,
90 },
91 EnumName,
92 EnumVariant {
93 enum_name: String,
94 },
95 Declaration,
96 FilteredEnumName {
97 enum_name: String,
98 },
99 Token {
100 name: String,
101 is_static: bool,
102 },
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Hash)]
106pub enum ResolvedTypeKey {
107 Token { name: String, is_static: bool },
108 Property { start: usize },
109}
110
111pub type ResolvedTypes = HashMap<ResolvedTypeKey, Datatype>;
112
113#[derive(Clone, Copy)]
114enum LhsKind<'a> {
115 Token { name: &'a str, is_static: bool },
116 Property { name: &'a str },
117}
118
119impl<'a> LhsKind<'a> {
120 fn name(&self) -> &'a str {
121 match *self {
122 LhsKind::Token { name, .. } | LhsKind::Property { name } => name,
123 }
124 }
125}
126
127fn strip_sigil_span(span: (usize, usize)) -> (usize, usize) {
131 let (start, end) = span;
132 (start.saturating_add(1).min(end), end)
133}
134
135impl DefinitionKind {
136 fn selector_hint(classes: &Vec<String>) -> String {
137 classes.join(" | ")
138 }
139
140 pub fn selector(type_definition: Vec<String>) -> Self {
141 let hint = Self::selector_hint(&type_definition);
142 Self::Selector {
143 type_definition,
144 hint,
145 }
146 }
147}
148
149pub struct TypecheckedRsml {
150 pub errors: AstErrors,
151 pub derives: HashMap<PathBuf, RangeInclusive<usize>>,
152 pub dependencies: HashSet<PathBuf>,
153 pub definitions: Definitions,
154 pub resolved_types: ResolvedTypes,
155}
156
157pub struct Typechecker<'a> {
158 pub parsed: &'a ParsedRsml<'a>,
159 macro_registry: MacroRegistry<'a>,
160 pub(crate) static_scopes: Vec<HashMap<String, Datatype>>,
161 pub(crate) declared_tokens: Vec<HashSet<ResolvedTypeKey>>,
162 pub(crate) language_mode: LanguageMode,
163}
164
165pub(crate) struct TypecheckerLookup<'a> {
166 pub scopes: &'a [HashMap<String, Datatype>],
167}
168
169impl<'a> StaticLookup for TypecheckerLookup<'a> {
170 fn resolve_static(&self, name: &str) -> Datatype {
171 for scope in self.scopes.iter().rev() {
172 if let Some(dt) = scope.get(name) {
173 return dt.clone();
174 }
175 }
176 Datatype::None
177 }
178
179 fn resolve_dynamic(&self, _name: &str) -> Datatype {
180 Datatype::None
181 }
182}
183
184impl<'a> Typechecker<'a> {
185 pub async fn new(
186 parsed: &'a ParsedRsml<'a>,
187 current_path: &Path,
188 mut luaurc: Option<&mut Luaurc>,
189 ) -> TypecheckedRsml {
190 let language_mode = parsed.directives.language_mode.unwrap_or_else(|| {
191 luaurc
192 .as_deref()
193 .map(|luaurc_ref| luaurc_ref.language_mode)
194 .unwrap_or_default()
195 });
196
197 let mut typechecker: Typechecker<'a> = Self {
198 parsed,
199 macro_registry: MacroRegistry::new(),
200 static_scopes: vec![HashMap::new()],
201 declared_tokens: vec![HashSet::new()],
202 language_mode,
203 };
204
205 let mut ast_errors = AstErrors::new();
208
209 let mut derives: HashMap<PathBuf, RangeInclusive<usize>> = HashMap::new();
210 let mut definitions = Definitions::new();
211 let mut resolved_types: ResolvedTypes = HashMap::new();
212 let mut dependencies = HashSet::new();
213
214 for construct in &typechecker.parsed.ast {
215 match construct {
216 Construct::Derive {
217 body: Some(derive_body),
218 ..
219 } => {
220 typechecker
221 .typecheck_derive(
222 derive_body,
223 &mut ast_errors,
224 current_path,
225 luaurc.as_deref_mut(),
226 &mut dependencies,
227 &mut derives,
228 )
229 .await;
230 }
231
232 Construct::Tween {
233 body: Some(body), ..
234 } => {
235 ast_errors.report(
236 TypeError::NotAllowedInContext {
237 name: construct.name_plural(),
238 context: "the global scope",
239 },
240 Range::from_span(&typechecker.parsed.rope, construct.span()),
241 );
242 typechecker.typecheck_tween(body, &mut ast_errors);
243 }
244
245 Construct::Rule { selectors, body } => {
246 typechecker.typecheck_rule(
247 (selectors, body),
248 &vec![],
249 &mut ast_errors,
250 &mut definitions,
251 &mut resolved_types,
252 );
253 }
254
255 Construct::Macro {
256 name,
257 args,
258 return_type,
259 body,
260 ..
261 } => {
262 'register: {
263 let Some(name_node) = name else { break 'register };
264 let Token::Identifier(name_str) = name_node.token.value() else {
265 break 'register;
266 };
267
268 let arg_names = collect_macro_def_arg_names(args);
269 let arg_count = arg_names.len();
270 let context = macro_return_context(return_type);
271 let key = MacroKey {
272 name: *name_str,
273 arity: arg_count,
274 };
275
276 let builtin_collision = !typechecker.parsed.directives.nobuiltins
277 && crate::builtins::BUILTINS.registry.contains_key(&key);
278
279 let local_collision = typechecker.macro_registry.contains_key(&key);
280
281 if builtin_collision || local_collision {
282 ast_errors.report(
283 TypeError::DuplicateMacro {
284 name: name_str,
285 arg_count,
286 },
287 Range::from_span(&typechecker.parsed.rope, construct.span()),
288 );
289 } else {
290 typechecker.macro_registry.insert(
291 key,
292 MacroDefinition {
293 arg_names,
294 body: body.as_ref().map(|b| &b.content),
295 return_context: context,
296 },
297 );
298 }
299 }
300
301 typechecker.typecheck_macro(args, body, &mut ast_errors);
302 }
303
304 Construct::MacroCall { name, body, .. } => {
305 typechecker.validate_macro_call(
306 name,
307 body,
308 MacroReturnContext::Construct,
309 &mut ast_errors,
310 );
311 }
312
313 Construct::Assignment {
314 left,
315 right: Some(right),
316 ..
317 } => {
318 if matches!(left.token.value(), Token::Identifier(_)) {
319 ast_errors.report(
320 TypeError::NotAllowedInContext {
321 name: construct.name_plural(),
322 context: "the global scope",
323 },
324 Range::from_span(&typechecker.parsed.rope, construct.span()),
325 );
326 }
327 typechecker.validate_token_refs(right, &mut ast_errors);
328 typechecker.validate_macro_arg_refs(right, None, &mut ast_errors);
329 typechecker.validate_annotation(right, &mut ast_errors);
330 if let Construct::MacroCall { name, body, .. } = right.as_ref() {
331 typechecker.validate_macro_call(
332 name,
333 body,
334 MacroReturnContext::Datatype,
335 &mut ast_errors,
336 );
337 }
338 typechecker.resolve_token_assignment(left, right, &[], &mut ast_errors, &mut definitions, &mut resolved_types);
339 }
340
341 Construct::Priority { .. } => {
342 ast_errors.report(
343 TypeError::NotAllowedInContext {
344 name: construct.name_plural(),
345 context: "the global scope",
346 },
347 Range::from_span(&typechecker.parsed.rope, construct.span()),
348 );
349 }
350
351 _ => (),
352 }
353 }
354
355 typechecker.detect_recursive_macro_calls(&mut ast_errors);
356
357 TypecheckedRsml {
358 errors: ast_errors,
359 derives,
360 dependencies,
361 definitions,
362 resolved_types,
363 }
364 }
365
366 pub(crate) fn resolve_token_assignment(
367 &mut self,
368 left: &Node<'a>,
369 right: &Construct<'a>,
370 current_classes: &[String],
371 ast_errors: &mut AstErrors,
372 definitions: &mut Definitions,
373 resolved_types: &mut ResolvedTypes,
374 ) {
375 let lhs_kind = match left.token.value() {
376 Token::TokenIdentifier(name) => LhsKind::Token { name: *name, is_static: false },
377 Token::StaticTokenIdentifier(name) => LhsKind::Token { name: *name, is_static: true },
378 Token::Identifier(name) => LhsKind::Property { name: *name },
379 _ => return,
380 };
381
382 let name = lhs_kind.name();
383
384 let enum_valid = self.validate_enum_refs(left, right, ast_errors);
387
388 let resolved_type = if !enum_valid {
389 Datatype::None
390 } else {
391 let lookup = TypecheckerLookup { scopes: &self.static_scopes };
392
393 let evaluated = match lhs_kind {
394 LhsKind::Token { .. } => {
395 if let Construct::Node { node } = right {
396 if let Token::StateSelectorOrEnumPart(Some(value)) = node.token.value() {
397 Some(Datatype::IncompleteEnumShorthand(value.to_string()))
398 } else {
399 evaluate_construct(right, Some(name), &lookup)
400 }
401 } else {
402 evaluate_construct(right, Some(name), &lookup)
403 }
404 }
405 LhsKind::Property { .. } => evaluate_construct(right, Some(name), &lookup),
406 };
407
408 match lhs_kind {
409 LhsKind::Token { is_static, .. } => match evaluated {
410 Some(Datatype::IncompleteEnumShorthand(variant)) => {
411 Datatype::IncompleteEnumShorthand(variant)
412 }
413 Some(d) if is_static => d,
414 Some(d) => d
415 .coerce_to_variant(Some(name))
416 .map(Datatype::Variant)
417 .unwrap_or(Datatype::None),
418 None => Datatype::None,
419 },
420 LhsKind::Property { .. } => match evaluated {
421 Some(d) => d
422 .coerce_to_variant(Some(name))
423 .map(Datatype::Variant)
424 .unwrap_or(Datatype::None),
425 None => Datatype::None,
426 },
427 }
428 };
429
430 let (start, end) = left.token.span();
431
432 match lhs_kind {
433 LhsKind::Token { is_static, .. } => {
434 if is_static {
435 if let Some(frame) = self.static_scopes.last_mut() {
436 frame.insert(name.to_string(), resolved_type.clone());
437 }
438 }
439
440 let key = ResolvedTypeKey::Token { name: name.to_string(), is_static };
441 resolved_types.insert(key.clone(), resolved_type);
442
443 if let Some(frame) = self.declared_tokens.last_mut() {
444 frame.insert(key);
445 }
446
447 definitions.insert(
448 start..=end,
449 DefinitionKind::Token { name: name.to_string(), is_static },
450 );
451 }
452 LhsKind::Property { .. } => {
453 self.check_property_against_reflection(
454 name,
455 &resolved_type,
456 current_classes,
457 left,
458 right,
459 ast_errors,
460 );
461
462 let type_definition = vec![resolved_type.type_name()];
463 resolved_types.insert(
464 ResolvedTypeKey::Property { start },
465 resolved_type,
466 );
467 definitions.insert(
468 start..=end,
469 DefinitionKind::Assignment {
470 property_name: name.to_string(),
471 type_definition,
472 },
473 );
474 }
475 }
476 }
477
478 fn check_property_against_reflection(
485 &self,
486 property_name: &str,
487 resolved_type: &Datatype,
488 current_classes: &[String],
489 left: &Node<'a>,
490 right: &Construct<'a>,
491 ast_errors: &mut AstErrors,
492 ) {
493 if current_classes.is_empty() {
494 return;
495 }
496
497 let Ok(db) = rbx_reflection_database::get() else {
498 return;
499 };
500
501 let mut descriptors: Vec<Option<&rbx_reflection::PropertyDescriptor>> =
502 Vec::with_capacity(current_classes.len());
503
504 let mut missing_classes: Vec<String> = Vec::new();
505 let mut present_classes: Vec<String> = Vec::new();
506
507 for class_name in current_classes {
508 let descriptor = properties::lookup_property(db, class_name, property_name);
509
510 if descriptor.is_some() {
511 present_classes.push(class_name.clone());
512 } else {
513 missing_classes.push(class_name.clone());
514 }
515
516 descriptors.push(descriptor);
517 }
518
519 let should_error = match self.language_mode {
520 LanguageMode::Strict => !missing_classes.is_empty(),
521 LanguageMode::Nonstrict => present_classes.is_empty(),
522 };
523
524 if should_error {
525 ast_errors.report(
526 TypeError::UnknownProperty {
527 name: property_name.to_string(),
528 missing: missing_classes,
529 present: present_classes,
530 },
531 Range::from_span(&self.parsed.rope, left.token.span()),
532 );
533 return;
534 }
535
536 let Datatype::Variant(value) = resolved_type else {
537 return;
538 };
539
540 let first_descriptor = descriptors
544 .iter()
545 .find_map(|descriptor| descriptor.as_ref().copied());
546
547 let Some(descriptor) = first_descriptor else {
548 return;
549 };
550
551 if properties::variant_matches(descriptor, value) {
552 return;
553 }
554
555 ast_errors.report(
556 TypeError::PropertyTypeMismatch {
557 name: property_name.to_string(),
558 expected: properties::expected_type_label(descriptor),
559 got: crate::datatype::variant_type_name(value.ty()).to_string(),
560 },
561 Range::from_span(&self.parsed.rope, right.span()),
562 );
563 }
564
565 pub(crate) fn validate_enum_refs(
571 &self,
572 left: &Node<'a>,
573 right: &Construct<'a>,
574 ast_errors: &mut AstErrors,
575 ) -> bool {
576 let mut ok = true;
577
578 'shorthand: {
580 let Construct::Node { node } = right else { break 'shorthand };
581 let Token::StateSelectorOrEnumPart(Some(variant)) = node.token.value() else {
582 break 'shorthand;
583 };
584
585 let lhs_name = match left.token.value() {
586 Token::Identifier(n)
587 | Token::TokenIdentifier(n)
588 | Token::StaticTokenIdentifier(n) => Some(*n),
589 _ => None,
590 };
591
592 if let Some(lhs_name) = lhs_name {
593 let enum_name = shorthand_rebind(lhs_name);
594 let variant_span = strip_sigil_span(node.token.span());
595 ok &= self.check_enum_name_and_variant(
596 enum_name,
597 variant,
598 variant_span,
599 variant_span,
600 ast_errors,
601 );
602 }
603
604 return ok;
605 }
606
607 ok &= self.validate_enum_refs_inner(right, ast_errors);
608 ok
609 }
610
611 fn validate_enum_refs_inner(
612 &self,
613 construct: &Construct<'a>,
614 ast_errors: &mut AstErrors,
615 ) -> bool {
616 let mut ok = true;
617 match construct {
618 Construct::Enum { name: Some(name_node), variant: Some(variant_node), .. } => {
619 let enum_name = annotations::enum_identifier(name_node.token.value());
620 let variant = annotations::enum_identifier(variant_node.token.value());
621
622 if let Some(enum_name) = enum_name {
623 let name_span = strip_sigil_span(name_node.token.span());
624 let variant_span = strip_sigil_span(variant_node.token.span());
625 ok &= self.check_enum_name_and_variant(
626 enum_name,
627 variant.unwrap_or(""),
628 name_span,
629 variant_span,
630 ast_errors,
631 );
632 }
633 }
634 Construct::MathOperation { left, right, .. } => {
635 ok &= self.validate_enum_refs_inner(left, ast_errors);
636 if let Some(right) = right {
637 ok &= self.validate_enum_refs_inner(right, ast_errors);
638 }
639 }
640 Construct::UnaryMinus { operand, .. } => {
641 ok &= self.validate_enum_refs_inner(operand, ast_errors);
642 }
643 Construct::Table { body } => {
644 ok &= self.validate_enum_refs_delimited(body, ast_errors);
645 }
646 Construct::AnnotatedTable { body: Some(body), .. } => {
647 ok &= self.validate_enum_refs_delimited(body, ast_errors);
648 }
649 Construct::MacroCall { body: Some(body), .. } => {
650 ok &= self.validate_enum_refs_delimited(body, ast_errors);
651 }
652 _ => {}
653 }
654 ok
655 }
656
657 fn validate_enum_refs_delimited(
658 &self,
659 delim: &Delimited<'a>,
660 ast_errors: &mut AstErrors,
661 ) -> bool {
662 let Some(content) = delim.content.as_ref() else {
663 return true;
664 };
665 let mut ok = true;
666 for item in content {
667 ok &= self.validate_enum_refs_inner(item, ast_errors);
668 }
669 ok
670 }
671
672 fn check_enum_name_and_variant(
673 &self,
674 enum_name: &str,
675 variant: &str,
676 name_span: (usize, usize),
677 variant_span: (usize, usize),
678 ast_errors: &mut AstErrors,
679 ) -> bool {
680 if !annotations::enum_exists(enum_name) {
681 ast_errors.report(
682 TypeError::UnknownEnum { name: enum_name.to_string() },
683 self.parsed.range_from_span(name_span),
684 );
685 return false;
686 }
687
688 if variant.is_empty() {
689 return true;
690 }
691
692 if !annotations::validate_enum_variant(variant, enum_name) {
693 ast_errors.report(
694 TypeError::UnknownEnumVariant {
695 enum_name: enum_name.to_string(),
696 variant: variant.to_string(),
697 },
698 self.parsed.range_from_span(variant_span),
699 );
700 return false;
701 }
702
703 true
704 }
705
706 pub(crate) fn validate_token_refs(
707 &self,
708 construct: &Construct<'a>,
709 ast_errors: &mut AstErrors,
710 ) {
711 match construct {
712 Construct::Node { node } => {
713 let (name, is_static) = match node.token.value() {
714 Token::TokenIdentifier(n) => (*n, false),
715 Token::StaticTokenIdentifier(n) => (*n, true),
716 _ => return,
717 };
718 let key = ResolvedTypeKey::Token {
719 name: name.to_string(),
720 is_static,
721 };
722 let in_scope = self
723 .declared_tokens
724 .iter()
725 .rev()
726 .any(|frame| frame.contains(&key));
727 if !in_scope {
728 ast_errors.report(
729 TypeError::UndefinedToken { name, is_static },
730 self.parsed.range_from_span(node.token.span()),
731 );
732 }
733 }
734 Construct::MathOperation { left, right, .. } => {
735 self.validate_token_refs(left, ast_errors);
736 if let Some(right) = right {
737 self.validate_token_refs(right, ast_errors);
738 }
739 }
740 Construct::UnaryMinus { operand, .. } => {
741 self.validate_token_refs(operand, ast_errors);
742 }
743 Construct::Table { body } => {
744 self.validate_token_refs_delimited(body, ast_errors);
745 }
746 Construct::AnnotatedTable {
747 body: Some(body), ..
748 } => {
749 self.validate_token_refs_delimited(body, ast_errors);
750 }
751 Construct::MacroCall {
752 body: Some(body), ..
753 } => {
754 self.validate_token_refs_delimited(body, ast_errors);
755 }
756 _ => {}
757 }
758 }
759
760 fn validate_token_refs_delimited(
761 &self,
762 delim: &Delimited<'a>,
763 ast_errors: &mut AstErrors,
764 ) {
765 let Some(content) = delim.content.as_ref() else {
766 return;
767 };
768 for item in content {
769 self.validate_token_refs(item, ast_errors);
770 }
771 }
772}
773
774#[cfg(test)]
775mod tests {
776 use crate::typechecker::*;
777 use crate::{lexer::RsmlLexer, parser::RsmlParser};
778
779 use std::path::PathBuf;
780
781 struct TypecheckResult {
782 selectors: Vec<(usize, usize, Vec<String>)>,
783 scopes: Vec<(usize, usize, Vec<String>)>,
784 tokens: Vec<(usize, usize, String, bool, Datatype)>,
785 properties: Vec<(usize, usize, String, Datatype)>,
786 errors: Vec<String>,
787 }
788
789 async fn typecheck(source: &str) -> TypecheckResult {
790 typecheck_with_luaurc(source, None).await
791 }
792
793 async fn typecheck_with_luaurc(
794 source: &str,
795 luaurc_contents: Option<&str>,
796 ) -> TypecheckResult {
797 let lexer = RsmlLexer::new(source);
798 let parsed = RsmlParser::new(lexer);
799 let dummy_path = PathBuf::from("/test.rsml");
800
801 let mut luaurc = luaurc_contents.map(Luaurc::new);
802
803 let TypecheckedRsml {
804 errors: ast_errors,
805 derives: _derives,
806 definitions,
807 dependencies: _dependencies,
808 resolved_types,
809 } = Typechecker::new(&parsed, &dummy_path, luaurc.as_mut()).await;
810
811 let selectors: Vec<(usize, usize, Vec<String>)> = definitions
812 .iter()
813 .filter_map(|(range, kind)| {
814 if let DefinitionKind::Selector {
815 type_definition, ..
816 } = kind
817 {
818 Some((*range.start(), *range.end(), type_definition.clone()))
819 } else {
820 None
821 }
822 })
823 .collect();
824
825 let scopes: Vec<(usize, usize, Vec<String>)> = definitions
826 .iter()
827 .filter_map(|(range, kind)| {
828 if let DefinitionKind::Scope {
829 type_definition, ..
830 } = kind
831 {
832 Some((*range.start(), *range.end(), type_definition.clone()))
833 } else {
834 None
835 }
836 })
837 .collect();
838
839 let tokens: Vec<(usize, usize, String, bool, Datatype)> = definitions
840 .iter()
841 .filter_map(|(range, kind)| {
842 if let DefinitionKind::Token { name, is_static } = kind {
843 let resolved_type = resolved_types
844 .get(&ResolvedTypeKey::Token {
845 name: name.clone(),
846 is_static: *is_static,
847 })
848 .cloned()
849 .unwrap_or(Datatype::None);
850 Some((
851 *range.start(),
852 *range.end(),
853 name.clone(),
854 *is_static,
855 resolved_type,
856 ))
857 } else {
858 None
859 }
860 })
861 .collect();
862
863 let properties: Vec<(usize, usize, String, Datatype)> = definitions
864 .iter()
865 .filter_map(|(range, kind)| {
866 if let DefinitionKind::Assignment { property_name, .. } = kind {
867 let resolved_type = resolved_types
868 .get(&ResolvedTypeKey::Property { start: *range.start() })
869 .cloned()
870 .unwrap_or(Datatype::None);
871 Some((
872 *range.start(),
873 *range.end(),
874 property_name.clone(),
875 resolved_type,
876 ))
877 } else {
878 None
879 }
880 })
881 .collect();
882
883 let errors: Vec<String> = ast_errors
884 .0
885 .iter()
886 .map(|diagnostic| diagnostic.message.clone())
887 .collect();
888
889 TypecheckResult {
890 selectors,
891 scopes,
892 tokens,
893 properties,
894 errors,
895 }
896 }
897
898 #[tokio::test]
899 async fn simple_class_selector() {
900 let result = typecheck("Frame {}").await;
901 assert_eq!(result.selectors.len(), 1);
902 assert_eq!(result.selectors[0].2, vec!["Frame"]);
903 assert!(result.errors.is_empty());
904 }
905
906 #[tokio::test]
907 async fn class_with_pseudo_selector() {
908 let result = typecheck("Frame ::UIPadding {}").await;
909 assert_eq!(result.selectors.len(), 1);
910 assert_eq!(result.selectors[0].2, vec!["UIPadding"]);
911 assert!(result.errors.is_empty());
912 }
913
914 #[tokio::test]
915 async fn class_with_state_selector() {
916 let result = typecheck("Frame :hover {}").await;
917 assert_eq!(result.selectors.len(), 1);
918 assert_eq!(result.selectors[0].2, vec!["Frame"]);
919 assert!(result.errors.is_empty());
920 }
921
922 #[tokio::test]
923 async fn comma_separated_selectors() {
924 let result = typecheck("Frame, TextButton {}").await;
925 assert_eq!(result.selectors.len(), 1);
926 assert_eq!(result.selectors[0].2, vec!["Frame", "TextButton"]);
927 assert!(result.errors.is_empty());
928 }
929
930 #[tokio::test]
931 async fn invalid_class_name() {
932 let result = typecheck("NotARealClass {}").await;
933 assert_eq!(result.selectors.len(), 1);
934 assert_eq!(result.selectors[0].2, vec!["Instance"]);
935 assert_eq!(result.errors.len(), 1);
936 assert!(result.errors[0].contains("No class named \"NotARealClass\" exists"));
937 }
938
939 #[tokio::test]
940 async fn invalid_pseudo_not_a_class() {
941 let result = typecheck("Frame ::NotAClass {}").await;
942 assert_eq!(result.selectors.len(), 1);
943 assert_eq!(result.selectors[0].2, vec!["Instance"]);
944 assert!(
945 result
946 .errors
947 .iter()
948 .any(|err| err.contains("No class named \"NotAClass\" exists"))
949 );
950 }
951
952 #[tokio::test]
953 async fn invalid_pseudo_not_allowed() {
954 let result = typecheck("Frame ::Frame {}").await;
955 assert_eq!(result.selectors.len(), 1);
956 assert_eq!(result.selectors[0].2, vec!["Frame"]);
957 assert!(
958 result
959 .errors
960 .iter()
961 .any(|err| err.contains("can't be used as a Pseudo instance"))
962 );
963 }
964
965 #[tokio::test]
966 async fn invalid_state_selector() {
967 let result = typecheck("Frame :notastate {}").await;
968 assert_eq!(result.selectors.len(), 1);
969 assert_eq!(result.selectors[0].2, vec!["Frame"]);
970 assert!(
971 result
972 .errors
973 .iter()
974 .any(|err| err.contains("No state named \"notastate\" exists"))
975 );
976 }
977
978 #[tokio::test]
979 async fn nested_class_without_combinator_errors() {
980 let result = typecheck("Frame { TextButton {} }").await;
981 assert_eq!(result.selectors.len(), 2);
982 assert_eq!(result.selectors[0].2, vec!["Frame"]);
983 assert_eq!(result.selectors[1].2, vec!["TextButton"]);
984 assert_eq!(result.errors.len(), 1);
985 assert!(result.errors[0].contains("can't be nested"));
986 }
987
988 #[tokio::test]
989 async fn nested_child_selector() {
990 let result = typecheck("Frame { > TextButton {} }").await;
991 assert_eq!(result.selectors.len(), 2);
992 assert_eq!(result.selectors[0].2, vec!["Frame"]);
993 assert_eq!(result.selectors[1].2, vec!["TextButton"]);
994 assert!(result.errors.is_empty());
995 }
996
997 #[tokio::test]
998 async fn nested_pseudo_selector() {
999 let result = typecheck("Frame { ::UIPadding {} }").await;
1000 assert_eq!(result.selectors.len(), 2);
1001 assert_eq!(result.selectors[0].2, vec!["Frame"]);
1002 assert_eq!(result.selectors[1].2, vec!["UIPadding"]);
1003 assert!(result.errors.is_empty());
1004 }
1005
1006 #[tokio::test]
1007 async fn nested_state_selector() {
1008 let result = typecheck("Frame { :hover {} }").await;
1009 assert_eq!(result.selectors.len(), 2);
1010 assert_eq!(result.selectors[0].2, vec!["Frame"]);
1011 assert_eq!(result.selectors[1].2, vec!["Frame"]);
1012 assert!(result.errors.is_empty());
1013 }
1014
1015 #[tokio::test]
1016 async fn multiple_nesting_levels() {
1017 let result = typecheck("Frame { TextButton { TextLabel {} } }").await;
1018 assert_eq!(result.selectors.len(), 3);
1019 assert_eq!(result.selectors[0].2, vec!["Frame"]);
1020 assert_eq!(result.selectors[1].2, vec!["TextButton"]);
1021 assert_eq!(result.selectors[2].2, vec!["TextLabel"]);
1022 assert_eq!(result.errors.len(), 2);
1023 assert!(
1024 result
1025 .errors
1026 .iter()
1027 .all(|err| err.contains("can't be nested"))
1028 );
1029 }
1030
1031 #[tokio::test]
1032 async fn nested_child_combinator_with_nesting() {
1033 let result = typecheck("Frame { > TextButton { > TextLabel {} } }").await;
1034 assert_eq!(result.selectors.len(), 3);
1035 assert_eq!(result.selectors[0].2, vec!["Frame"]);
1036 assert_eq!(result.selectors[1].2, vec!["TextButton"]);
1037 assert_eq!(result.selectors[2].2, vec!["TextLabel"]);
1038 assert!(result.errors.is_empty());
1039 }
1040
1041 #[tokio::test]
1042 async fn top_level_child_selector_resolves_to_child() {
1043 let result = typecheck("Frame > TextButton {}").await;
1044 assert_eq!(result.selectors.len(), 1);
1045 assert_eq!(result.selectors[0].2, vec!["TextButton"]);
1046 assert!(result.errors.is_empty());
1047 }
1048
1049 #[tokio::test]
1050 async fn top_level_child_with_pseudo_resolves_to_pseudo() {
1051 let result = typecheck("Frame > TextButton ::UIPadding {}").await;
1052 assert_eq!(result.selectors.len(), 1);
1053 assert_eq!(result.selectors[0].2, vec!["UIPadding"]);
1054 assert!(result.errors.is_empty());
1055 }
1056
1057 #[tokio::test]
1058 async fn top_level_child_with_state_resolves_to_child() {
1059 let result = typecheck("Frame > TextButton :hover {}").await;
1060 assert_eq!(result.selectors.len(), 1);
1061 assert_eq!(result.selectors[0].2, vec!["TextButton"]);
1062 assert!(result.errors.is_empty());
1063 }
1064
1065 #[tokio::test]
1066 async fn top_level_chain_with_name_selector_coerces_to_instance() {
1067 let result = typecheck("Frame > TextButton > .Hello {}").await;
1068 assert_eq!(result.selectors.len(), 1);
1069 assert_eq!(result.selectors[0].2, vec!["Instance"]);
1070 assert!(result.errors.is_empty());
1071 }
1072
1073 #[tokio::test]
1074 async fn top_level_child_with_name_selector_coerces_to_instance() {
1075 let result = typecheck("Frame > .Hello {}").await;
1076 assert_eq!(result.selectors.len(), 1);
1077 assert_eq!(result.selectors[0].2, vec!["Instance"]);
1078 assert!(result.errors.is_empty());
1079 }
1080
1081 #[tokio::test]
1082 async fn nested_child_with_name_selector_coerces_to_instance() {
1083 let result = typecheck("Frame { > .Hello {} }").await;
1084 assert_eq!(result.selectors.len(), 2);
1085 assert_eq!(result.selectors[0].2, vec!["Frame"]);
1086 assert_eq!(result.selectors[1].2, vec!["Instance"]);
1087 assert!(result.errors.is_empty());
1088 }
1089
1090 #[tokio::test]
1091 async fn chain_with_tag_then_comma() {
1092 let result = typecheck("Frame >> TextButton > .Hello, Frame {}").await;
1093 assert_eq!(result.selectors.len(), 1);
1094 assert_eq!(result.selectors[0].2, vec!["Instance", "Frame"]);
1095 assert!(result.errors.is_empty());
1096 }
1097
1098 #[tokio::test]
1099 async fn tag_selector_then_comma_at_top_level() {
1100 let result = typecheck(".Hello, TextButton {}").await;
1101 assert_eq!(result.selectors.len(), 1);
1102 assert_eq!(result.selectors[0].2, vec!["Instance", "TextButton"]);
1103 assert!(result.errors.is_empty());
1104 }
1105
1106 #[tokio::test]
1107 async fn nested_tag_then_comma() {
1108 let result = typecheck("Frame { > .Hello, > TextButton {} }").await;
1109 assert_eq!(result.selectors.len(), 2);
1110 assert_eq!(result.selectors[0].2, vec!["Frame"]);
1111 assert_eq!(result.selectors[1].2, vec!["Instance", "TextButton"]);
1112 assert!(result.errors.is_empty());
1113 }
1114
1115 #[tokio::test]
1116 async fn duplicate_comma_selectors_are_deduplicated() {
1117 let result = typecheck("Frame, Frame, TextButton {}").await;
1118 assert_eq!(result.selectors.len(), 1);
1119 assert_eq!(result.selectors[0].2, vec!["Frame", "TextButton"]);
1120 assert!(result.errors.is_empty());
1121 }
1122
1123 #[tokio::test]
1124 async fn all_duplicate_selectors() {
1125 let result = typecheck("Frame, Frame, Frame {}").await;
1126 assert_eq!(result.selectors.len(), 1);
1127 assert_eq!(result.selectors[0].2, vec!["Frame"]);
1128 assert!(result.errors.is_empty());
1129 }
1130
1131 #[tokio::test]
1132 async fn duplicate_with_combinator() {
1133 let result = typecheck("Frame > TextButton, Frame > TextButton {}").await;
1134 assert_eq!(result.selectors.len(), 1);
1135 assert_eq!(result.selectors[0].2, vec!["TextButton"]);
1136 assert!(result.errors.is_empty());
1137 }
1138
1139 #[tokio::test]
1140 async fn duplicate_instance_coercion() {
1141 let result = typecheck(".Hello, .World {}").await;
1142 assert_eq!(result.selectors.len(), 1);
1143 assert_eq!(result.selectors[0].2, vec!["Instance"]);
1144 assert!(result.errors.is_empty());
1145 }
1146
1147 #[tokio::test]
1148 async fn duplicate_with_state_selectors() {
1149 let result = typecheck("Frame :hover, Frame :press {}").await;
1150 assert_eq!(result.selectors.len(), 1);
1151 assert_eq!(result.selectors[0].2, vec!["Frame"]);
1152 assert!(result.errors.is_empty());
1153 }
1154
1155 #[tokio::test]
1156 async fn duplicate_pseudo_selectors() {
1157 let result = typecheck("Frame ::UIPadding, TextButton ::UIPadding {}").await;
1158 assert_eq!(result.selectors.len(), 1);
1159 assert_eq!(result.selectors[0].2, vec!["UIPadding"]);
1160 assert!(result.errors.is_empty());
1161 }
1162
1163 #[tokio::test]
1164 async fn nested_duplicate_selectors() {
1165 let result = typecheck("Frame { > TextButton, > TextButton {} }").await;
1166 assert_eq!(result.selectors.len(), 2);
1167 assert_eq!(result.selectors[0].2, vec!["Frame"]);
1168 assert_eq!(result.selectors[1].2, vec!["TextButton"]);
1169 assert!(result.errors.is_empty());
1170 }
1171
1172 #[tokio::test]
1173 async fn no_dedup_different_types() {
1174 let result = typecheck("Frame, TextButton {}").await;
1175 assert_eq!(result.selectors.len(), 1);
1176 assert_eq!(result.selectors[0].2, vec!["Frame", "TextButton"]);
1177 assert!(result.errors.is_empty());
1178 }
1179
1180 #[tokio::test]
1181 async fn preserves_order_after_dedup() {
1182 let result = typecheck("TextButton, Frame, TextButton {}").await;
1183 assert_eq!(result.selectors.len(), 1);
1184 assert_eq!(result.selectors[0].2, vec!["TextButton", "Frame"]);
1185 assert!(result.errors.is_empty());
1186 }
1187
1188 #[tokio::test]
1189 async fn scope_inserted_for_rule_body() {
1190 let result = typecheck("Frame {}").await;
1191 assert_eq!(result.scopes.len(), 1);
1192 assert_eq!(result.scopes[0].2, vec!["Frame"]);
1193 }
1194
1195 #[tokio::test]
1196 async fn scope_has_union_types() {
1197 let result = typecheck("Frame, TextButton {}").await;
1198 assert_eq!(result.scopes.len(), 1);
1199 assert_eq!(result.scopes[0].2, vec!["Frame", "TextButton"]);
1200 }
1201
1202 #[tokio::test]
1203 async fn nested_scopes_have_correct_types() {
1204 let result = typecheck("Frame { > TextButton {} }").await;
1205 assert!(result.scopes.len() >= 2);
1208 let scope_types: Vec<&Vec<String>> = result.scopes.iter().map(|s| &s.2).collect();
1209 assert!(scope_types.contains(&&vec!["Frame".to_string()]));
1210 assert!(scope_types.contains(&&vec!["TextButton".to_string()]));
1211 }
1212
1213 #[tokio::test]
1214 async fn scope_with_combinator() {
1215 let result = typecheck("Frame > TextButton {}").await;
1216 assert_eq!(result.scopes.len(), 1);
1217 assert_eq!(result.scopes[0].2, vec!["TextButton"]);
1218 }
1219
1220 #[tokio::test]
1221 async fn scope_with_pseudo_selector() {
1222 let result = typecheck("Frame ::UIPadding {}").await;
1223 assert_eq!(result.scopes.len(), 1);
1224 assert_eq!(result.scopes[0].2, vec!["UIPadding"]);
1225 }
1226
1227 #[tokio::test]
1228 async fn top_level_state_selector_resolves_to_instance() {
1229 let result = typecheck(":hover {}").await;
1230 assert_eq!(result.selectors.len(), 1);
1231 assert_eq!(result.selectors[0].2, vec!["Instance"]);
1232 assert!(result.errors.is_empty());
1233 }
1234
1235 #[tokio::test]
1236 async fn top_level_state_selector_invalid_state() {
1237 let result = typecheck(":notastate {}").await;
1238 assert_eq!(result.selectors.len(), 1);
1239 assert_eq!(result.selectors[0].2, vec!["Instance"]);
1240 assert!(
1241 result
1242 .errors
1243 .iter()
1244 .any(|err| err.contains("No state named \"notastate\" exists"))
1245 );
1246 }
1247
1248 #[tokio::test]
1249 async fn nested_state_selector_inherits_parent_class() {
1250 let result = typecheck("Frame { :hover {} }").await;
1251 assert_eq!(result.selectors.len(), 2);
1252 assert_eq!(result.selectors[0].2, vec!["Frame"]);
1253 assert_eq!(result.selectors[1].2, vec!["Frame"]);
1254 assert!(result.errors.is_empty());
1255 }
1256
1257 #[tokio::test]
1258 async fn top_level_pseudo_selector_resolves_instance_type() {
1259 let result = typecheck("::UIPadding {}").await;
1260 assert_eq!(result.selectors.len(), 1);
1261 assert_eq!(result.selectors[0].2, vec!["UIPadding"]);
1262 assert!(result.errors.is_empty());
1263 }
1264
1265 #[tokio::test]
1266 async fn top_level_pseudo_selector_scope_resolves() {
1267 let result = typecheck("::UIPadding {}").await;
1268 assert_eq!(result.scopes.len(), 1);
1269 assert_eq!(result.scopes[0].2, vec!["UIPadding"]);
1270 }
1271
1272 #[tokio::test]
1273 async fn top_level_pseudo_selector_invalid_class() {
1274 let result = typecheck("::NotARealClass {}").await;
1275 assert_eq!(result.selectors.len(), 1);
1276 assert_eq!(result.selectors[0].2, vec!["Instance"]);
1277 assert!(
1278 result
1279 .errors
1280 .iter()
1281 .any(|err| err.contains("No class named \"NotARealClass\" exists"))
1282 );
1283 }
1284
1285 #[tokio::test]
1286 async fn top_level_pseudo_selector_not_allowed_class() {
1287 let result = typecheck("::Frame {}").await;
1288 assert_eq!(result.selectors.len(), 1);
1289 assert_eq!(result.selectors[0].2, vec!["Frame"]);
1290 assert!(
1291 result
1292 .errors
1293 .iter()
1294 .any(|err| err.contains("can't be used as a Pseudo instance"))
1295 );
1296 }
1297
1298 #[tokio::test]
1299 async fn top_level_pseudo_selectors_with_comma() {
1300 let result = typecheck("::UIPadding, ::UICorner {}").await;
1301 assert_eq!(result.selectors.len(), 1);
1302 assert_eq!(result.selectors[0].2, vec!["UIPadding", "UICorner"]);
1303 assert!(result.errors.is_empty());
1304 }
1305
1306 #[tokio::test]
1307 async fn comma_after_state_selector_continues() {
1308 let result = typecheck("Frame :hover, TextButton :hover {}").await;
1309 assert_eq!(result.selectors.len(), 1);
1310 assert_eq!(result.selectors[0].2, vec!["Frame", "TextButton"]);
1311 assert!(result.errors.is_empty());
1312 }
1313
1314 #[tokio::test]
1315 async fn comma_after_pseudo_selector_continues() {
1316 let result = typecheck("Frame ::UIPadding, TextButton ::UICorner {}").await;
1317 assert_eq!(result.selectors.len(), 1);
1318 assert_eq!(result.selectors[0].2, vec!["UIPadding", "UICorner"]);
1319 assert!(result.errors.is_empty());
1320 }
1321
1322 #[tokio::test]
1323 async fn nested_comma_after_state_selector_continues() {
1324 let result = typecheck("Frame { > TextButton :hover, > TextLabel :press {} }").await;
1325 assert_eq!(result.selectors.len(), 2);
1326 assert_eq!(result.selectors[1].2, vec!["TextButton", "TextLabel"]);
1327 assert!(result.errors.is_empty());
1328 }
1329
1330 #[tokio::test]
1331 async fn nested_comma_after_pseudo_selector_continues() {
1332 let result = typecheck("Frame { ::UIPadding, ::UICorner {} }").await;
1333 assert_eq!(result.selectors.len(), 2);
1334 assert_eq!(result.selectors[1].2, vec!["UIPadding", "UICorner"]);
1335 assert!(result.errors.is_empty());
1336 }
1337
1338 #[tokio::test]
1339 async fn nested_standalone_pseudo_selector_resolves() {
1340 let result = typecheck("Frame { ::UIPadding {} }").await;
1341 assert_eq!(result.selectors.len(), 2);
1342 assert_eq!(result.selectors[0].2, vec!["Frame"]);
1343 assert_eq!(result.selectors[1].2, vec!["UIPadding"]);
1344 assert!(result.errors.is_empty());
1345 }
1346
1347 #[tokio::test]
1348 async fn nested_comma_after_state_selector_inherits_parent() {
1349 let result = typecheck("Frame { :hover, :press {} }").await;
1350 assert_eq!(result.selectors.len(), 2);
1351 assert_eq!(result.selectors[1].2, vec!["Frame"]);
1352 assert!(result.errors.is_empty());
1353 }
1354
1355 #[tokio::test]
1356 async fn nested_comma_pseudo_with_class_prefix() {
1357 let result =
1358 typecheck("Frame { > TextButton ::UIPadding, > TextLabel ::UICorner {} }").await;
1359 assert_eq!(result.selectors.len(), 2);
1360 assert_eq!(result.selectors[1].2, vec!["UIPadding", "UICorner"]);
1361 assert!(result.errors.is_empty());
1362 }
1363
1364 #[tokio::test]
1365 async fn macro_arg_nonexistent_errors() {
1366 let result =
1367 typecheck("@macro Padding (&x) { ::UIPadding { PaddingTop = &nonexistent; } }").await;
1368 assert!(
1369 result
1370 .errors
1371 .iter()
1372 .any(|err| err.contains("No macro argument named"))
1373 );
1374 }
1375
1376 #[tokio::test]
1377 async fn macro_arg_valid_no_error() {
1378 let result =
1379 typecheck("@macro MyPadding (&all) { ::UIPadding { PaddingTop = &all; } }").await;
1380 let macro_errors: Vec<_> = result
1381 .errors
1382 .iter()
1383 .filter(|err| err.contains("Macro"))
1384 .collect();
1385 assert!(
1386 macro_errors.is_empty(),
1387 "unexpected macro errors: {:?}",
1388 macro_errors
1389 );
1390 }
1391
1392 #[tokio::test]
1393 async fn macro_arg_outside_macro_errors() {
1394 let result = typecheck("Frame { PaddingTop = &all; }").await;
1395 assert!(
1396 result
1397 .errors
1398 .iter()
1399 .any(|err| err.contains("No macro argument named \"all\" exists."))
1400 );
1401 }
1402
1403 #[tokio::test]
1404 async fn macro_call_after_definition_no_error() {
1405 let result = typecheck("@macro Padding () { ::UIPadding {} }\nPadding!();").await;
1406 let macro_errors: Vec<_> = result
1407 .errors
1408 .iter()
1409 .filter(|err| err.contains("Undefined Macro") || err.contains("Wrong Macro"))
1410 .collect();
1411 assert!(
1412 macro_errors.is_empty(),
1413 "unexpected macro errors: {:?}",
1414 macro_errors
1415 );
1416 }
1417
1418 #[tokio::test]
1419 async fn macro_call_before_definition_errors() {
1420 let result = typecheck("MyPadding!();\n@macro MyPadding () { ::UIPadding {} }").await;
1421 assert!(
1422 result
1423 .errors
1424 .iter()
1425 .any(|err| err.contains("No macro named `MyPadding` has been defined"))
1426 );
1427 }
1428
1429 #[tokio::test]
1430 async fn macro_call_undefined_errors() {
1431 let result = typecheck("DoesNotExist!();").await;
1432 assert!(
1433 result
1434 .errors
1435 .iter()
1436 .any(|err| err.contains("No macro named `DoesNotExist` has been defined"))
1437 );
1438 }
1439
1440 #[tokio::test]
1441 async fn macro_call_wrong_arg_count_errors() {
1442 let result = typecheck("@macro Padding (&all) { ::UIPadding {} }\nPadding!();").await;
1443 assert!(
1444 result
1445 .errors
1446 .iter()
1447 .any(|err| err.contains("Wrong Macro Argument Count"))
1448 );
1449 }
1450
1451 #[tokio::test]
1452 async fn macro_call_correct_arg_count_no_error() {
1453 let result = typecheck("@macro Padding (&all) { ::UIPadding {} }\nPadding!(10);").await;
1454 let macro_errors: Vec<_> = result
1455 .errors
1456 .iter()
1457 .filter(|err| err.contains("Wrong Macro Argument Count"))
1458 .collect();
1459 assert!(
1460 macro_errors.is_empty(),
1461 "unexpected errors: {:?}",
1462 macro_errors
1463 );
1464 }
1465
1466 #[tokio::test]
1467 async fn macro_call_overloaded_correct_arg_count() {
1468 let result = typecheck(
1469 "@macro Padding (&all) { ::UIPadding {} }\n@macro Padding (&x, &y) { ::UIPadding {} }\nPadding!(1, 2);"
1470 ).await;
1471 let macro_errors: Vec<_> = result
1472 .errors
1473 .iter()
1474 .filter(|err| err.contains("Wrong Macro Argument Count"))
1475 .collect();
1476 assert!(
1477 macro_errors.is_empty(),
1478 "unexpected errors: {:?}",
1479 macro_errors
1480 );
1481 }
1482
1483 #[tokio::test]
1484 async fn macro_call_overloaded_wrong_arg_count() {
1485 let result = typecheck(
1486 "@macro MyPadding (&all) { ::UIPadding {} }\n@macro MyPadding (&x, &y) { ::UIPadding {} }\nMyPadding!(1, 2, 3);"
1487 ).await;
1488 assert!(
1489 result
1490 .errors
1491 .iter()
1492 .any(|err| err.contains("Wrong Macro Argument Count"))
1493 );
1494 }
1495
1496 #[tokio::test]
1497 async fn macro_call_construct_in_datatype_context_errors() {
1498 let result = typecheck("@macro Foo () { Frame {} }\nFrame { Size = Foo!(); }").await;
1499 assert!(
1500 result
1501 .errors
1502 .iter()
1503 .any(|err| err.contains("Wrong Macro Context"))
1504 );
1505 }
1506
1507 #[tokio::test]
1508 async fn macro_call_datatype_in_construct_context_errors() {
1509 let result = typecheck("@macro Foo () -> Datatype { 10 }\nFoo!();").await;
1510 assert!(
1511 result
1512 .errors
1513 .iter()
1514 .any(|err| err.contains("Wrong Macro Context"))
1515 );
1516 }
1517
1518 #[tokio::test]
1519 async fn macro_call_datatype_in_datatype_context_no_error() {
1520 let result =
1521 typecheck("@macro Foo () -> Datatype { 10 }\nFrame { Size = Foo!(); }").await;
1522 let macro_errors: Vec<_> = result
1523 .errors
1524 .iter()
1525 .filter(|err| err.contains("Wrong Macro Context"))
1526 .collect();
1527 assert!(
1528 macro_errors.is_empty(),
1529 "unexpected errors: {:?}",
1530 macro_errors
1531 );
1532 }
1533
1534 #[tokio::test]
1535 async fn macro_call_selector_in_selector_context_no_error() {
1536 let result = typecheck("@macro Sel () -> Selector { Frame }\nSel!() {}").await;
1537 let macro_errors: Vec<_> = result
1538 .errors
1539 .iter()
1540 .filter(|err| err.contains("Wrong Macro Context") || err.contains("Undefined Macro"))
1541 .collect();
1542 assert!(
1543 macro_errors.is_empty(),
1544 "unexpected errors: {:?}",
1545 macro_errors
1546 );
1547 }
1548
1549 #[tokio::test]
1550 async fn macro_call_selector_in_selector_context_with_comma_no_error() {
1551 let result = typecheck("@macro Sel () -> Selector { Frame }\nFrame, Sel!() {}").await;
1552 let macro_errors: Vec<_> = result
1553 .errors
1554 .iter()
1555 .filter(|err| err.contains("Wrong Macro Context") || err.contains("Undefined Macro"))
1556 .collect();
1557 assert!(
1558 macro_errors.is_empty(),
1559 "unexpected errors: {:?}",
1560 macro_errors
1561 );
1562 }
1563
1564 #[tokio::test]
1565 async fn macro_call_construct_in_selector_context_errors() {
1566 let result = typecheck("@macro Foo () { Frame {} }\nFoo!() {}").await;
1567 assert!(
1568 result
1569 .errors
1570 .iter()
1571 .any(|err| err.contains("Wrong Macro Context"))
1572 );
1573 }
1574
1575 #[tokio::test]
1576 async fn macro_call_in_rule_body() {
1577 let result = typecheck("@macro Padding () { ::UIPadding {} }\nFrame { Padding!(); }").await;
1578 let macro_errors: Vec<_> = result
1579 .errors
1580 .iter()
1581 .filter(|err| err.contains("Undefined Macro") || err.contains("Wrong Macro"))
1582 .collect();
1583 assert!(
1584 macro_errors.is_empty(),
1585 "unexpected errors: {:?}",
1586 macro_errors
1587 );
1588 }
1589
1590 #[tokio::test]
1591 async fn macro_call_no_return_type_defaults_to_construct() {
1592 let result = typecheck("@macro Foo () { Frame {} }\nFoo!();").await;
1593 let macro_errors: Vec<_> = result
1594 .errors
1595 .iter()
1596 .filter(|err| err.contains("Wrong Macro Context") || err.contains("Undefined Macro"))
1597 .collect();
1598 assert!(
1599 macro_errors.is_empty(),
1600 "unexpected errors: {:?}",
1601 macro_errors
1602 );
1603 }
1604
1605 #[tokio::test]
1606 async fn macro_call_inside_macro_body() {
1607 let result =
1608 typecheck("@macro Inner () { ::UIPadding {} }\n@macro Outer () { Inner!(); }").await;
1609 let macro_errors: Vec<_> = result
1610 .errors
1611 .iter()
1612 .filter(|err| err.contains("Undefined Macro") || err.contains("Wrong Macro"))
1613 .collect();
1614 assert!(
1615 macro_errors.is_empty(),
1616 "unexpected errors: {:?}",
1617 macro_errors
1618 );
1619 }
1620
1621 #[tokio::test]
1622 async fn macro_call_inside_macro_body_undefined_errors() {
1623 let result = typecheck("@macro Outer () { NotDefined!(); }").await;
1624 assert!(
1625 result
1626 .errors
1627 .iter()
1628 .any(|err| err.contains("No macro named `NotDefined` has been defined"))
1629 );
1630 }
1631
1632 #[tokio::test]
1633 async fn macro_duplicate_same_name_same_args_errors() {
1634 let result =
1635 typecheck("@macro Test () { Frame {} }\n@macro Test () -> Selector { Frame }").await;
1636 assert!(
1637 result
1638 .errors
1639 .iter()
1640 .any(|err| err.contains("Duplicate Macro"))
1641 );
1642 }
1643
1644 #[tokio::test]
1645 async fn macro_duplicate_same_name_different_args_no_error() {
1646 let result =
1647 typecheck("@macro Test (&a) { Frame {} }\n@macro Test (&a, &b) { Frame {} }").await;
1648 let duplicate_errors: Vec<_> = result
1649 .errors
1650 .iter()
1651 .filter(|err| err.contains("Duplicate Macro"))
1652 .collect();
1653 assert!(
1654 duplicate_errors.is_empty(),
1655 "unexpected errors: {:?}",
1656 duplicate_errors
1657 );
1658 }
1659
1660 #[tokio::test]
1661 async fn macro_direct_recursion_emits_error() {
1662 let result = typecheck("@macro Foo() -> Construct { Foo!(); }\nFrame { Foo!(); }").await;
1663 let recursive: Vec<_> = result
1664 .errors
1665 .iter()
1666 .filter(|err| err.contains("Recursive Macro Call"))
1667 .collect();
1668 assert_eq!(
1669 recursive.len(),
1670 1,
1671 "expected exactly one recursive-call error, got: {:?}",
1672 recursive
1673 );
1674 }
1675
1676 #[tokio::test]
1677 async fn macro_indirect_recursion_emits_error() {
1678 let result = typecheck(
1679 "@macro A() -> Construct { B!(); }\n@macro B() -> Construct { A!(); }\nFrame { A!(); }",
1680 )
1681 .await;
1682 let recursive: Vec<_> = result
1683 .errors
1684 .iter()
1685 .filter(|err| err.contains("Recursive Macro Call"))
1686 .collect();
1687 assert_eq!(
1688 recursive.len(),
1689 1,
1690 "expected exactly one recursive-call error on the cycle-closing edge, got: {:?}",
1691 recursive
1692 );
1693 }
1694
1695 #[tokio::test]
1696 async fn macro_selector_direct_recursion_emits_error() {
1697 let result = typecheck("@macro Sel -> Selector { Sel!() }\nSel!() { }").await;
1698 let recursive: Vec<_> = result
1699 .errors
1700 .iter()
1701 .filter(|err| err.contains("Recursive Macro Call"))
1702 .collect();
1703 assert_eq!(
1704 recursive.len(),
1705 1,
1706 "expected exactly one recursive-call error, got: {:?}",
1707 recursive
1708 );
1709 }
1710
1711 #[tokio::test]
1712 async fn macro_overload_cross_arity_not_recursive() {
1713 let result = typecheck(
1714 "@macro Foo() -> Construct { Foo!(10px); }\n@macro Foo(&v) -> Construct { ::Inner { X = &v; } }\nFrame { Foo!(); }",
1715 )
1716 .await;
1717 let recursive: Vec<_> = result
1718 .errors
1719 .iter()
1720 .filter(|err| err.contains("Recursive Macro Call"))
1721 .collect();
1722 assert!(
1723 recursive.is_empty(),
1724 "cross-arity overload should not report recursion, got: {:?}",
1725 recursive
1726 );
1727 }
1728
1729 #[tokio::test]
1730 async fn macro_call_selector_no_args_no_error() {
1731 let result = typecheck("@macro Foo -> Selector { }\nFoo!() {}").await;
1732 let macro_errors: Vec<_> = result
1733 .errors
1734 .iter()
1735 .filter(|err| err.contains("Wrong Macro Context") || err.contains("Undefined Macro"))
1736 .collect();
1737 assert!(
1738 macro_errors.is_empty(),
1739 "unexpected errors: {:?}",
1740 macro_errors
1741 );
1742 }
1743
1744 #[tokio::test]
1745 async fn macro_call_selector_no_args_with_comma_no_error() {
1746 let result = typecheck("@macro Foo -> Selector { }\nFoo!(), Frame {}").await;
1747 let macro_errors: Vec<_> = result
1748 .errors
1749 .iter()
1750 .filter(|err| err.contains("Wrong Macro Context") || err.contains("Undefined Macro"))
1751 .collect();
1752 assert!(
1753 macro_errors.is_empty(),
1754 "unexpected errors: {:?}",
1755 macro_errors
1756 );
1757 }
1758
1759 #[tokio::test]
1760 async fn builtin_padding_one_arg_no_error() {
1761 let result = typecheck("Frame { Padding!(10); }").await;
1762 let macro_errors: Vec<_> = result
1763 .errors
1764 .iter()
1765 .filter(|err| err.contains("Undefined Macro") || err.contains("Wrong Macro"))
1766 .collect();
1767 assert!(
1768 macro_errors.is_empty(),
1769 "unexpected errors: {:?}",
1770 macro_errors
1771 );
1772 }
1773
1774 #[tokio::test]
1775 async fn builtin_padding_two_args_no_error() {
1776 let result = typecheck("Frame { Padding!(10, 20); }").await;
1777 let macro_errors: Vec<_> = result
1778 .errors
1779 .iter()
1780 .filter(|err| err.contains("Undefined Macro") || err.contains("Wrong Macro"))
1781 .collect();
1782 assert!(
1783 macro_errors.is_empty(),
1784 "unexpected errors: {:?}",
1785 macro_errors
1786 );
1787 }
1788
1789 #[tokio::test]
1790 async fn builtin_padding_three_args_no_error() {
1791 let result = typecheck("Frame { Padding!(10, 20, 30); }").await;
1792 let macro_errors: Vec<_> = result
1793 .errors
1794 .iter()
1795 .filter(|err| err.contains("Undefined Macro") || err.contains("Wrong Macro"))
1796 .collect();
1797 assert!(
1798 macro_errors.is_empty(),
1799 "unexpected errors: {:?}",
1800 macro_errors
1801 );
1802 }
1803
1804 #[tokio::test]
1805 async fn builtin_padding_four_args_no_error() {
1806 let result = typecheck("Frame { Padding!(10, 20, 30, 40); }").await;
1807 let macro_errors: Vec<_> = result
1808 .errors
1809 .iter()
1810 .filter(|err| err.contains("Undefined Macro") || err.contains("Wrong Macro"))
1811 .collect();
1812 assert!(
1813 macro_errors.is_empty(),
1814 "unexpected errors: {:?}",
1815 macro_errors
1816 );
1817 }
1818
1819 #[tokio::test]
1820 async fn builtin_corner_radius_no_error() {
1821 let result = typecheck("Frame { CornerRadius!(8); }").await;
1822 let macro_errors: Vec<_> = result
1823 .errors
1824 .iter()
1825 .filter(|err| err.contains("Undefined Macro") || err.contains("Wrong Macro"))
1826 .collect();
1827 assert!(
1828 macro_errors.is_empty(),
1829 "unexpected errors: {:?}",
1830 macro_errors
1831 );
1832 }
1833
1834 #[tokio::test]
1835 async fn builtin_scale_no_error() {
1836 let result = typecheck("Frame { Scale!(1.5); }").await;
1837 let macro_errors: Vec<_> = result
1838 .errors
1839 .iter()
1840 .filter(|err| err.contains("Undefined Macro") || err.contains("Wrong Macro"))
1841 .collect();
1842 assert!(
1843 macro_errors.is_empty(),
1844 "unexpected errors: {:?}",
1845 macro_errors
1846 );
1847 }
1848
1849 #[tokio::test]
1850 async fn builtin_padding_zero_args_errors() {
1851 let result = typecheck("Frame { Padding!(); }").await;
1852 let err = result
1853 .errors
1854 .iter()
1855 .find(|err| err.contains("Wrong Macro Argument Count"))
1856 .expect("expected wrong arg count error");
1857 assert!(
1858 err.contains("1, 2, 3, or 4 arguments"),
1859 "expected Oxford-comma arg list, got: {}",
1860 err
1861 );
1862 }
1863
1864 #[tokio::test]
1865 async fn builtin_padding_five_args_errors() {
1866 let result = typecheck("Frame { Padding!(10, 20, 30, 40, 50); }").await;
1867 assert!(
1868 result
1869 .errors
1870 .iter()
1871 .any(|err| err.contains("Wrong Macro Argument Count")),
1872 "expected arg count error, got: {:?}",
1873 result.errors
1874 );
1875 }
1876
1877 #[tokio::test]
1878 async fn builtin_corner_radius_wrong_arg_count_errors() {
1879 let result = typecheck("Frame { CornerRadius!(); }").await;
1880 assert!(
1881 result
1882 .errors
1883 .iter()
1884 .any(|err| err.contains("Wrong Macro Argument Count")
1885 && err.contains("CornerRadius")),
1886 "expected arg count error, got: {:?}",
1887 result.errors
1888 );
1889 }
1890
1891 #[tokio::test]
1892 async fn builtin_user_redefine_padding_duplicate_errors() {
1893 let result = typecheck("@macro Padding (&all) { ::UIPadding {} }").await;
1894 assert!(
1895 result
1896 .errors
1897 .iter()
1898 .any(|err| err.contains("Duplicate Macro") && err.contains("Padding")),
1899 "expected duplicate macro error, got: {:?}",
1900 result.errors
1901 );
1902 }
1903
1904 #[tokio::test]
1905 async fn builtin_padding_in_assignment_context_errors() {
1906 let result = typecheck("Frame { Size = Padding!(10); }").await;
1907 assert!(
1908 result
1909 .errors
1910 .iter()
1911 .any(|err| err.contains("Wrong Macro Context")),
1912 "expected wrong context error, got: {:?}",
1913 result.errors
1914 );
1915 }
1916
1917 #[tokio::test]
1918 async fn builtin_padding_in_selector_context_errors() {
1919 let result = typecheck("Padding!(10) {}").await;
1920 assert!(
1921 result
1922 .errors
1923 .iter()
1924 .any(|err| err.contains("Wrong Macro Context")),
1925 "expected wrong context error, got: {:?}",
1926 result.errors
1927 );
1928 }
1929
1930 #[tokio::test]
1931 async fn builtin_undefined_macro_still_errors() {
1932 let result = typecheck("Frame { NotABuiltin!(10); }").await;
1933 assert!(
1934 result
1935 .errors
1936 .iter()
1937 .any(|err| err.contains("No macro named `NotABuiltin` has been defined")),
1938 "expected undefined macro error, got: {:?}",
1939 result.errors
1940 );
1941 }
1942
1943 #[tokio::test]
1944 async fn annotation_unknown_name_errors() {
1945 let result = typecheck("Frame { Size = notareal(1, 2); }").await;
1946 assert!(
1947 result
1948 .errors
1949 .iter()
1950 .any(|err| err.contains("Unknown Annotation") && err.contains("notareal")),
1951 "expected unknown annotation error, got: {:?}",
1952 result.errors
1953 );
1954 }
1955
1956 #[tokio::test]
1957 async fn annotation_valid_udim2_no_error() {
1958 let result = typecheck("Frame { Size = udim2(1, 0, 1, 0); }").await;
1959 let annotation_errors: Vec<_> = result
1960 .errors
1961 .iter()
1962 .filter(|err| err.contains("Annotation"))
1963 .collect();
1964 assert!(
1965 annotation_errors.is_empty(),
1966 "unexpected errors: {:?}",
1967 annotation_errors
1968 );
1969 }
1970
1971 #[tokio::test]
1972 async fn annotation_valid_vec3_no_error() {
1973 let result = typecheck("Frame { Position = vec3(1, 2, 3); }").await;
1974 let annotation_errors: Vec<_> = result
1975 .errors
1976 .iter()
1977 .filter(|err| err.contains("Annotation"))
1978 .collect();
1979 assert!(
1980 annotation_errors.is_empty(),
1981 "unexpected errors: {:?}",
1982 annotation_errors
1983 );
1984 }
1985
1986 #[tokio::test]
1987 async fn annotation_too_many_args_errors() {
1988 let result = typecheck("Frame { Size = vec2(1, 2, 3); }").await;
1989 assert!(
1990 result
1991 .errors
1992 .iter()
1993 .any(|err| err.contains("Wrong Annotation Argument Count")),
1994 "expected arg count error, got: {:?}",
1995 result.errors
1996 );
1997 }
1998
1999 #[tokio::test]
2000 async fn annotation_too_few_args_errors() {
2001 let result = typecheck("Frame { Size = lerp(); }").await;
2002 assert!(
2003 result
2004 .errors
2005 .iter()
2006 .any(|err| err.contains("Wrong Annotation Argument Count")),
2007 "expected arg count error, got: {:?}",
2008 result.errors
2009 );
2010 }
2011
2012 #[tokio::test]
2013 async fn annotation_wrong_arg_type_errors() {
2014 let result = typecheck("Frame { Size = vec2(\"hello\", \"world\"); }").await;
2015 assert!(
2016 result
2017 .errors
2018 .iter()
2019 .any(|err| err.contains("Wrong Annotation Argument Type")),
2020 "expected arg type error, got: {:?}",
2021 result.errors
2022 );
2023 }
2024
2025 #[tokio::test]
2026 async fn annotation_variadic_colorseq_many_args() {
2027 let result = typecheck("Frame { Color = colorseq(#ff0000, #00ff00, #0000ff); }").await;
2028 let annotation_errors: Vec<_> = result
2029 .errors
2030 .iter()
2031 .filter(|err| err.contains("Annotation"))
2032 .collect();
2033 assert!(
2034 annotation_errors.is_empty(),
2035 "unexpected errors: {:?}",
2036 annotation_errors
2037 );
2038 }
2039
2040 #[tokio::test]
2041 async fn annotation_variadic_colorseq_empty_errors() {
2042 let result = typecheck("Frame { Color = colorseq(); }").await;
2043 assert!(
2044 result
2045 .errors
2046 .iter()
2047 .any(|err| err.contains("Wrong Annotation Argument Count")),
2048 "expected arg count error, got: {:?}",
2049 result.errors
2050 );
2051 }
2052
2053 #[tokio::test]
2054 async fn annotation_nested_annotation_validated() {
2055 let result = typecheck("Frame { Size = udim2(vec2(1, 2, 3), 0); }").await;
2056 assert!(
2057 result
2058 .errors
2059 .iter()
2060 .any(|err| err.contains("Wrong Annotation Argument Count")),
2061 "expected nested vec2 arg count error, got: {:?}",
2062 result.errors
2063 );
2064 }
2065
2066 #[tokio::test]
2067 async fn annotation_case_insensitive_matching() {
2068 let result = typecheck("Frame { Size = UDim2(1, 0, 1, 0); }").await;
2069 let annotation_errors: Vec<_> = result
2070 .errors
2071 .iter()
2072 .filter(|err| err.contains("Annotation"))
2073 .collect();
2074 assert!(
2075 annotation_errors.is_empty(),
2076 "unexpected errors: {:?}",
2077 annotation_errors
2078 );
2079 }
2080
2081 #[tokio::test]
2082 async fn annotation_zero_args_errors() {
2083 let result = typecheck("Frame { Color = brickcolor(); }").await;
2084 assert!(
2085 result
2086 .errors
2087 .iter()
2088 .any(|err| err.contains("Wrong Annotation Argument Count")),
2089 "expected arg count error, got: {:?}",
2090 result.errors
2091 );
2092 }
2093
2094 #[tokio::test]
2095 async fn annotation_color3_accepts_color_arg() {
2096 let result = typecheck("Frame { BackgroundColor3 = color3(#ff0000); }").await;
2097 let annotation_errors: Vec<_> = result
2098 .errors
2099 .iter()
2100 .filter(|err| err.contains("Annotation"))
2101 .collect();
2102 assert!(
2103 annotation_errors.is_empty(),
2104 "unexpected errors: {:?}",
2105 annotation_errors
2106 );
2107 }
2108
2109 #[tokio::test]
2110 async fn annotation_color3_three_numbers() {
2111 let result = typecheck("Frame { BackgroundColor3 = color3(1, 0, 0); }").await;
2112 let annotation_errors: Vec<_> = result
2113 .errors
2114 .iter()
2115 .filter(|err| err.contains("Annotation"))
2116 .collect();
2117 assert!(
2118 annotation_errors.is_empty(),
2119 "unexpected errors: {:?}",
2120 annotation_errors
2121 );
2122 }
2123
2124 #[tokio::test]
2125 async fn annotation_udim2_with_percent_scale() {
2126 let result = typecheck("Frame { Size = udim2(50%, 50%); }").await;
2127 let annotation_errors: Vec<_> = result
2128 .errors
2129 .iter()
2130 .filter(|err| err.contains("Annotation"))
2131 .collect();
2132 assert!(
2133 annotation_errors.is_empty(),
2134 "unexpected errors: {:?}",
2135 annotation_errors
2136 );
2137 }
2138
2139 #[tokio::test]
2140 async fn annotation_font_with_enum() {
2141 let result =
2142 typecheck("Frame { FontFace = font(\"rbxasset://fonts/arial.ttf\", Enum.FontWeight.Bold); }")
2143 .await;
2144 let annotation_errors: Vec<_> = result
2145 .errors
2146 .iter()
2147 .filter(|err| err.contains("Annotation"))
2148 .collect();
2149 assert!(
2150 annotation_errors.is_empty(),
2151 "unexpected errors: {:?}",
2152 annotation_errors
2153 );
2154 }
2155
2156 #[tokio::test]
2157 async fn annotation_at_top_level_is_validated() {
2158 let result = typecheck("$Size = vec2(1, 2, 3);").await;
2159 assert!(
2160 result
2161 .errors
2162 .iter()
2163 .any(|err| err.contains("Wrong Annotation Argument Count")),
2164 "expected arg count error, got: {:?}",
2165 result.errors
2166 );
2167 }
2168
2169 #[tokio::test]
2170 async fn annotation_in_macro_body_is_validated() {
2171 let result =
2172 typecheck("@macro Foo () { Frame { Size = vec2(1, 2, 3); } }").await;
2173 assert!(
2174 result
2175 .errors
2176 .iter()
2177 .any(|err| err.contains("Wrong Annotation Argument Count")),
2178 "expected arg count error, got: {:?}",
2179 result.errors
2180 );
2181 }
2182
2183 #[tokio::test]
2184 async fn annotation_token_arg_errors() {
2185 let result = typecheck("Frame { Size = udim2($Width, 0, 1, 0); }").await;
2186 assert!(
2187 result
2188 .errors
2189 .iter()
2190 .any(|err| err.contains("Tokens are not allowed in tuple annotations")),
2191 "expected token-in-annotation error, got: {:?}",
2192 result.errors
2193 );
2194 }
2195
2196 #[tokio::test]
2197 async fn annotation_token_nested_inside_math_errors() {
2198 let result = typecheck("Frame { Size = udim2($Width + 10, 0, 1, 0); }").await;
2199 assert!(
2200 result
2201 .errors
2202 .iter()
2203 .any(|err| err.contains("Tokens are not allowed in tuple annotations")),
2204 "expected token-in-annotation error, got: {:?}",
2205 result.errors
2206 );
2207 }
2208
2209 #[tokio::test]
2210 async fn annotation_static_token_arg_allowed() {
2211 let result = typecheck("Frame { Size = udim2($!Width, 0, 1, 0); }").await;
2212 let token_errors: Vec<_> = result
2213 .errors
2214 .iter()
2215 .filter(|err| err.contains("Tokens are not allowed"))
2216 .collect();
2217 assert!(
2218 token_errors.is_empty(),
2219 "unexpected static-token error: {:?}",
2220 token_errors
2221 );
2222 }
2223
2224 fn annotation_arg_type_errors(result: &TypecheckResult) -> Vec<&String> {
2225 result
2226 .errors
2227 .iter()
2228 .filter(|err| err.contains("must be"))
2229 .collect()
2230 }
2231
2232 #[tokio::test]
2233 async fn annotation_static_token_measurement_valid() {
2234 let result = typecheck("$!W = 100; Frame { Size = udim2($!W, 0%); }").await;
2235 let errs = annotation_arg_type_errors(&result);
2236 assert!(errs.is_empty(), "unexpected arg-type errors: {:?}", errs);
2237 }
2238
2239 #[tokio::test]
2240 async fn annotation_static_token_scale_measurement_valid() {
2241 let result = typecheck("$!Hello = 50%; Frame { Hello = udim2(50%, $!Hello); }").await;
2242 let errs = annotation_arg_type_errors(&result);
2243 assert!(errs.is_empty(), "unexpected arg-type errors: {:?}", errs);
2244 }
2245
2246 #[tokio::test]
2247 async fn annotation_static_token_number_valid() {
2248 let result = typecheck("$!N = 10; Frame { Size = vec3($!N, $!N, $!N); }").await;
2249 let errs = annotation_arg_type_errors(&result);
2250 assert!(errs.is_empty(), "unexpected arg-type errors: {:?}", errs);
2251 }
2252
2253 #[tokio::test]
2254 async fn annotation_static_token_color_valid() {
2255 let result =
2256 typecheck("$!C = #ff0000; Frame { BackgroundColor3 = color3($!C); }").await;
2257 let errs = annotation_arg_type_errors(&result);
2258 assert!(errs.is_empty(), "unexpected arg-type errors: {:?}", errs);
2259 }
2260
2261 #[tokio::test]
2262 async fn annotation_static_token_oklab_color_valid() {
2263 let result =
2264 typecheck("$!C = tw:red:500; Frame { BackgroundColor3 = color3($!C); }").await;
2265 let errs = annotation_arg_type_errors(&result);
2266 assert!(errs.is_empty(), "unexpected arg-type errors: {:?}", errs);
2267 }
2268
2269 #[tokio::test]
2270 async fn annotation_static_token_wrong_type_errors() {
2271 let result = typecheck("$!S = \"hi\"; Frame { Size = udim2($!S, 0%); }").await;
2272 let errs = annotation_arg_type_errors(&result);
2273 assert!(
2274 !errs.is_empty(),
2275 "expected a Wrong Annotation Argument Type error, got: {:?}",
2276 result.errors
2277 );
2278 }
2279
2280 #[tokio::test]
2281 async fn annotation_static_token_unresolved_permissive() {
2282 let result = typecheck("Frame { Size = udim2($!Unknown, 0%); }").await;
2283 let errs = annotation_arg_type_errors(&result);
2284 let token_errs: Vec<_> = result
2285 .errors
2286 .iter()
2287 .filter(|err| err.contains("Tokens are not allowed"))
2288 .collect();
2289 assert!(errs.is_empty(), "unexpected arg-type errors: {:?}", errs);
2290 assert!(token_errs.is_empty(), "unexpected token errors: {:?}", token_errs);
2291 }
2292
2293 #[tokio::test]
2294 async fn annotation_regular_token_still_errors() {
2295 let result = typecheck("$W = 10; Frame { Size = udim2($W, 0, 1, 0); }").await;
2296 assert!(
2297 result
2298 .errors
2299 .iter()
2300 .any(|err| err.contains("Tokens are not allowed")),
2301 "expected token-in-annotation error, got: {:?}",
2302 result.errors
2303 );
2304 }
2305
2306 #[tokio::test]
2307 async fn annotation_static_token_in_math_permissive() {
2308 let result = typecheck("$!W = 10; Frame { Size = udim2($!W + 5, 0%); }").await;
2309 let errs = annotation_arg_type_errors(&result);
2310 assert!(errs.is_empty(), "unexpected arg-type errors: {:?}", errs);
2311 }
2312
2313 #[tokio::test]
2314 async fn annotation_static_token_enum_valid() {
2315 let result = typecheck(
2316 "$!B = Enum.FontWeight.Bold; Frame { FontFace = font(\"rbxasset://fonts/arial.ttf\", $!B); }",
2317 )
2318 .await;
2319 let errs = annotation_arg_type_errors(&result);
2320 assert!(errs.is_empty(), "unexpected arg-type errors: {:?}", errs);
2321 }
2322
2323 #[tokio::test]
2324 async fn annotation_static_token_enum_wrong_type_errors() {
2325 let result =
2326 typecheck("$!B = Enum.FontWeight.Bold; Frame { Size = udim2($!B, 0%); }").await;
2327 let errs = annotation_arg_type_errors(&result);
2328 assert!(
2329 !errs.is_empty(),
2330 "expected a Wrong Annotation Argument Type error, got: {:?}",
2331 result.errors
2332 );
2333 }
2334
2335 fn find_token<'a>(
2336 result: &'a TypecheckResult,
2337 name: &str,
2338 is_static: bool,
2339 ) -> &'a Datatype {
2340 result
2341 .tokens
2342 .iter()
2343 .find(|(_, _, n, s, _)| n == name && *s == is_static)
2344 .map(|(_, _, _, _, dt)| dt)
2345 .unwrap_or_else(|| {
2346 panic!(
2347 "no token `{}` (static={}) found; tokens={:?}",
2348 name,
2349 is_static,
2350 result.tokens.iter().map(|(_, _, n, s, _)| (n, s)).collect::<Vec<_>>()
2351 )
2352 })
2353 }
2354
2355 fn find_property<'a>(result: &'a TypecheckResult, name: &str) -> &'a Datatype {
2356 result
2357 .properties
2358 .iter()
2359 .find(|(_, _, n, _)| n == name)
2360 .map(|(_, _, _, dt)| dt)
2361 .unwrap_or_else(|| {
2362 panic!(
2363 "no property `{}` found; properties={:?}",
2364 name,
2365 result.properties.iter().map(|(_, _, n, _)| n).collect::<Vec<_>>()
2366 )
2367 })
2368 }
2369
2370 #[tokio::test]
2371 async fn token_number_type() {
2372 let result = typecheck("$X = 10;").await;
2373 let dt = find_token(&result, "X", false);
2374 assert!(
2375 matches!(dt, Datatype::Variant(rbx_types::Variant::Float64(n)) if *n == 10.0),
2376 "got {:?}",
2377 dt
2378 );
2379 }
2380
2381 #[tokio::test]
2382 async fn token_color_hex_coerces_for_regular() {
2383 let result = typecheck("$X = #ff0000;").await;
2384 let dt = find_token(&result, "X", false);
2385 assert!(
2386 matches!(dt, Datatype::Variant(rbx_types::Variant::Color3(_))),
2387 "got {:?}",
2388 dt
2389 );
2390 }
2391
2392 #[tokio::test]
2393 async fn token_color_tailwind_coerces_to_color3() {
2394 let result = typecheck("$X = tw:red:500;").await;
2395 let dt = find_token(&result, "X", false);
2396 assert!(
2397 matches!(dt, Datatype::Variant(rbx_types::Variant::Color3(_))),
2398 "got {:?}",
2399 dt
2400 );
2401 }
2402
2403 #[tokio::test]
2404 async fn static_token_keeps_oklab() {
2405 let result = typecheck("$!X = tw:red:500;").await;
2406 let dt = find_token(&result, "X", true);
2407 assert!(matches!(dt, Datatype::Oklab(_)), "got {:?}", dt);
2408 }
2409
2410 #[tokio::test]
2411 async fn token_udim2() {
2412 let result = typecheck("$X = udim2(1, 0, 1, 0);").await;
2413 let dt = find_token(&result, "X", false);
2414 assert!(
2415 matches!(dt, Datatype::Variant(rbx_types::Variant::UDim2(_))),
2416 "got {:?}",
2417 dt
2418 );
2419 }
2420
2421 #[tokio::test]
2422 async fn token_string() {
2423 let result = typecheck("$X = \"hi\";").await;
2424 let dt = find_token(&result, "X", false);
2425 assert!(
2426 matches!(dt, Datatype::Variant(rbx_types::Variant::String(s)) if s == "hi"),
2427 "got {:?}",
2428 dt
2429 );
2430 }
2431
2432 #[tokio::test]
2433 async fn static_token_cross_ref() {
2434 let result = typecheck("$!A = 10; $!B = $!A;").await;
2435 let a = find_token(&result, "A", true);
2436 let b = find_token(&result, "B", true);
2437 assert!(
2438 matches!(a, Datatype::Variant(rbx_types::Variant::Float64(n)) if *n == 10.0),
2439 "got A={:?}",
2440 a
2441 );
2442 assert!(
2443 matches!(b, Datatype::Variant(rbx_types::Variant::Float64(n)) if *n == 10.0),
2444 "got B={:?}",
2445 b
2446 );
2447 }
2448
2449 #[tokio::test]
2450 async fn static_token_math() {
2451 let result = typecheck("$!A = 10; $!B = $!A + 5;").await;
2452 let b = find_token(&result, "B", true);
2453 assert!(
2454 matches!(b, Datatype::Variant(rbx_types::Variant::Float64(n)) if *n == 15.0),
2455 "got {:?}",
2456 b
2457 );
2458 }
2459
2460 #[tokio::test]
2461 async fn token_inside_rule_body() {
2462 let result = typecheck("Frame { $X = 10; }").await;
2463 let dt = find_token(&result, "X", false);
2464 assert!(
2465 matches!(dt, Datatype::Variant(rbx_types::Variant::Float64(n)) if *n == 10.0),
2466 "got {:?}",
2467 dt
2468 );
2469 }
2470
2471 #[tokio::test]
2472 async fn static_token_parent_scope_lookup() {
2473 let result = typecheck("$!A = 10; Frame { $!B = $!A; }").await;
2474 let b = find_token(&result, "B", true);
2475 assert!(
2476 matches!(b, Datatype::Variant(rbx_types::Variant::Float64(n)) if *n == 10.0),
2477 "got {:?}",
2478 b
2479 );
2480 }
2481
2482 #[tokio::test]
2483 async fn regular_token_ref_is_unknown() {
2484 let result = typecheck("$A = 10; $B = $A;").await;
2485 let b = find_token(&result, "B", false);
2486 assert!(matches!(b, Datatype::None), "got {:?}", b);
2487 }
2488
2489 #[tokio::test]
2490 async fn token_invalid_rhs() {
2491 let result = typecheck("$X = ;").await;
2492 if let Some((_, _, _, _, dt)) = result
2493 .tokens
2494 .iter()
2495 .find(|(_, _, n, s, _)| n == "X" && !*s)
2496 {
2497 assert!(matches!(dt, Datatype::None), "got {:?}", dt);
2498 }
2499 }
2500
2501 #[tokio::test]
2502 async fn token_enum_shorthand_dynamic_unknown_enum() {
2503 let result = typecheck("$X = :Hello;").await;
2504 let dt = find_token(&result, "X", false);
2505 assert!(matches!(dt, Datatype::None), "got {:?}", dt);
2506 assert!(
2507 result.errors.iter().any(|err| err.contains("Unknown Enum")),
2508 "expected Unknown Enum error, got: {:?}",
2509 result.errors
2510 );
2511 }
2512
2513 #[tokio::test]
2514 async fn token_enum_shorthand_static_unknown_enum() {
2515 let result = typecheck("$!X = :Hello;").await;
2516 let dt = find_token(&result, "X", true);
2517 assert!(matches!(dt, Datatype::None), "got {:?}", dt);
2518 assert!(
2519 result.errors.iter().any(|err| err.contains("Unknown Enum")),
2520 "expected Unknown Enum error, got: {:?}",
2521 result.errors
2522 );
2523 }
2524
2525 #[tokio::test]
2526 async fn token_full_enum_valid_dynamic() {
2527 let result = typecheck("$X = Enum.Material.Plastic;").await;
2528 let dt = find_token(&result, "X", false);
2529 assert!(
2530 matches!(
2531 dt,
2532 Datatype::Variant(rbx_types::Variant::EnumItem(item)) if item.ty == "Material"
2533 ),
2534 "got {:?}",
2535 dt
2536 );
2537 }
2538
2539 #[tokio::test]
2540 async fn token_full_enum_valid_static() {
2541 let result = typecheck("$!X = Enum.Material.Plastic;").await;
2542 let dt = find_token(&result, "X", true);
2543 assert!(
2544 matches!(
2545 dt,
2546 Datatype::Variant(rbx_types::Variant::EnumItem(item)) if item.ty == "Material"
2547 ),
2548 "got {:?}",
2549 dt
2550 );
2551 }
2552
2553 #[tokio::test]
2554 async fn token_full_enum_unresolvable_dynamic() {
2555 let result = typecheck("$X = Enum.NotReal.xyz;").await;
2556 let dt = find_token(&result, "X", false);
2557 assert!(matches!(dt, Datatype::None), "got {:?}", dt);
2558 assert!(
2559 result.errors.iter().any(|err| err.contains("Unknown Enum")),
2560 "expected Unknown Enum error, got: {:?}",
2561 result.errors
2562 );
2563 }
2564
2565 #[tokio::test]
2566 async fn token_full_enum_unresolvable_static() {
2567 let result = typecheck("$!X = Enum.NotReal.xyz;").await;
2568 let dt = find_token(&result, "X", true);
2569 assert!(matches!(dt, Datatype::None), "got {:?}", dt);
2570 assert!(
2571 result.errors.iter().any(|err| err.contains("Unknown Enum")),
2572 "expected Unknown Enum error, got: {:?}",
2573 result.errors
2574 );
2575 }
2576
2577 #[tokio::test]
2578 async fn token_boolean_dynamic() {
2579 let result = typecheck("$X = true;").await;
2580 let dt = find_token(&result, "X", false);
2581 assert!(
2582 matches!(dt, Datatype::Variant(rbx_types::Variant::Bool(true))),
2583 "got {:?}",
2584 dt
2585 );
2586 }
2587
2588 #[tokio::test]
2589 async fn static_token_oklch_not_coerced() {
2590 let result = typecheck("$!X = oklch(0.5, 0.1, 180);").await;
2591 let dt = find_token(&result, "X", true);
2592 assert!(matches!(dt, Datatype::Oklch(_)), "got {:?}", dt);
2593 }
2594
2595 fn has_undefined_token_error(result: &TypecheckResult) -> bool {
2596 result.errors.iter().any(|err| err.contains("Undefined Token"))
2597 }
2598
2599 #[tokio::test]
2600 async fn undefined_dynamic_token_direct() {
2601 let result = typecheck("$A = $nope;").await;
2602 assert!(
2603 has_undefined_token_error(&result),
2604 "expected Undefined Token error, got: {:?}",
2605 result.errors
2606 );
2607 }
2608
2609 #[tokio::test]
2610 async fn undefined_static_token_direct() {
2611 let result = typecheck("$!A = $!nope;").await;
2612 assert!(
2613 has_undefined_token_error(&result),
2614 "expected Undefined Token error, got: {:?}",
2615 result.errors
2616 );
2617 }
2618
2619 #[tokio::test]
2620 async fn undefined_token_in_property_assignment() {
2621 let result = typecheck("Frame { Size = $nope; }").await;
2622 assert!(
2623 has_undefined_token_error(&result),
2624 "expected Undefined Token error, got: {:?}",
2625 result.errors
2626 );
2627 }
2628
2629 #[tokio::test]
2630 async fn undefined_token_in_annotated_tuple() {
2631 let result = typecheck("Frame { Size = udim2(0%, $!Hello, 0%, 0%); }").await;
2632 assert!(
2633 has_undefined_token_error(&result),
2634 "expected Undefined Token error, got: {:?}",
2635 result.errors
2636 );
2637 }
2638
2639 #[tokio::test]
2640 async fn undefined_token_in_math() {
2641 let result = typecheck("$!A = 10; $!B = $!A + $!nope;").await;
2642 assert!(
2643 has_undefined_token_error(&result),
2644 "expected Undefined Token error, got: {:?}",
2645 result.errors
2646 );
2647 }
2648
2649 #[tokio::test]
2650 async fn undefined_token_in_table() {
2651 let result = typecheck("$A = { $nope };").await;
2652 assert!(
2653 has_undefined_token_error(&result),
2654 "expected Undefined Token error, got: {:?}",
2655 result.errors
2656 );
2657 }
2658
2659 #[tokio::test]
2660 async fn same_statement_self_ref_errors() {
2661 let result = typecheck("$A = $A;").await;
2662 assert!(
2663 has_undefined_token_error(&result),
2664 "expected Undefined Token error, got: {:?}",
2665 result.errors
2666 );
2667 }
2668
2669 #[tokio::test]
2670 async fn dynamic_and_static_distinct_keys() {
2671 let result = typecheck("$A = 10; $B = $!A;").await;
2672 assert!(
2673 has_undefined_token_error(&result),
2674 "expected Undefined Token error, got: {:?}",
2675 result.errors
2676 );
2677 }
2678
2679 #[tokio::test]
2680 async fn defined_static_token_no_error() {
2681 let result = typecheck("$!A = 10; $!B = $!A;").await;
2682 assert!(
2683 !has_undefined_token_error(&result),
2684 "unexpected Undefined Token error, got: {:?}",
2685 result.errors
2686 );
2687 }
2688
2689 #[tokio::test]
2690 async fn defined_dynamic_token_no_error() {
2691 let result = typecheck("$A = 10; Frame { Size = $A; }").await;
2692 assert!(
2693 !has_undefined_token_error(&result),
2694 "unexpected Undefined Token error, got: {:?}",
2695 result.errors
2696 );
2697 }
2698
2699 #[tokio::test]
2700 async fn nested_rule_inherits_outer_token() {
2701 let result = typecheck("$!A = 10; Frame { $!B = $!A; }").await;
2702 assert!(
2703 !has_undefined_token_error(&result),
2704 "unexpected Undefined Token error, got: {:?}",
2705 result.errors
2706 );
2707 }
2708
2709 #[tokio::test]
2710 async fn inner_shadow_resolves_to_outer_in_same_rhs() {
2711 let result = typecheck("$!A = 10; Frame { $!A = $!A; }").await;
2712 assert!(
2713 !has_undefined_token_error(&result),
2714 "unexpected Undefined Token error, got: {:?}",
2715 result.errors
2716 );
2717 }
2718
2719 #[tokio::test]
2720 async fn declared_unknown_static_token_errors_in_annotation_arg() {
2721 let result =
2722 typecheck("$!Hello = Enum.Hello.world; Frame { Size = udim2(50%, $!Hello); }").await;
2723 let errs = annotation_arg_type_errors(&result);
2724 assert!(
2725 !errs.is_empty(),
2726 "expected a Wrong Annotation Argument Type error, got: {:?}",
2727 result.errors
2728 );
2729 }
2730
2731 #[tokio::test]
2732 async fn same_scope_redeclaration_still_declared() {
2733 let result = typecheck("$A = 10; $A = 20; Frame { Size = $A; }").await;
2734 assert!(
2735 !has_undefined_token_error(&result),
2736 "unexpected Undefined Token error, got: {:?}",
2737 result.errors
2738 );
2739 }
2740
2741 fn has_unknown_enum_error(result: &TypecheckResult) -> bool {
2742 result.errors.iter().any(|err| err.contains("Unknown Enum"))
2743 }
2744
2745 #[tokio::test]
2746 async fn property_shorthand_unknown_enum_name() {
2747 let result = typecheck("Frame { Hello = :World; }").await;
2748 assert!(
2749 has_unknown_enum_error(&result),
2750 "expected Unknown Enum error, got: {:?}",
2751 result.errors
2752 );
2753 let dt = find_property(&result, "Hello");
2754 assert!(matches!(dt, Datatype::None), "got {:?}", dt);
2755 }
2756
2757 #[tokio::test]
2758 async fn property_full_enum_unknown_name() {
2759 let result = typecheck("Frame { Foo = Enum.Hello.World; }").await;
2760 assert!(
2761 has_unknown_enum_error(&result),
2762 "expected Unknown Enum error, got: {:?}",
2763 result.errors
2764 );
2765 let dt = find_property(&result, "Foo");
2766 assert!(matches!(dt, Datatype::None), "got {:?}", dt);
2767 }
2768
2769 #[tokio::test]
2770 async fn token_full_enum_unknown_variant() {
2771 let result = typecheck("$X = Enum.Material.NotAVariant;").await;
2772 let dt = find_token(&result, "X", false);
2773 assert!(matches!(dt, Datatype::None), "got {:?}", dt);
2774 assert!(
2775 result
2776 .errors
2777 .iter()
2778 .any(|err| err.contains("Unknown Enum Variant")),
2779 "expected Unknown Enum Variant error, got: {:?}",
2780 result.errors
2781 );
2782 }
2783
2784 #[tokio::test]
2785 async fn property_full_enum_valid() {
2786 let result = typecheck("Frame { Material = Enum.Material.Plastic; }").await;
2787 assert!(
2788 !has_unknown_enum_error(&result),
2789 "unexpected Unknown Enum error, got: {:?}",
2790 result.errors
2791 );
2792 let dt = find_property(&result, "Material");
2793 assert!(
2794 matches!(
2795 dt,
2796 Datatype::Variant(rbx_types::Variant::EnumItem(item)) if item.ty == "Material"
2797 ),
2798 "got {:?}",
2799 dt
2800 );
2801 }
2802
2803 fn has_unknown_property_error(result: &TypecheckResult) -> bool {
2804 result
2805 .errors
2806 .iter()
2807 .any(|err| err.contains("Unknown Property"))
2808 }
2809
2810 fn has_property_type_mismatch_error(result: &TypecheckResult) -> bool {
2811 result
2812 .errors
2813 .iter()
2814 .any(|err| err.contains("Property Type Mismatch"))
2815 }
2816
2817 #[tokio::test]
2818 async fn property_matching_reflection_type_no_error() {
2819 let result = typecheck("Frame { Position = UDim2.new(0, 0, 0, 0); }").await;
2820 assert!(
2821 !has_unknown_property_error(&result) && !has_property_type_mismatch_error(&result),
2822 "unexpected property diagnostics, got: {:?}",
2823 result.errors
2824 );
2825 }
2826
2827 #[tokio::test]
2828 async fn property_type_mismatch_emits_error() {
2829 let result = typecheck("Frame { Position = \"hello\"; }").await;
2830 assert!(
2831 has_property_type_mismatch_error(&result),
2832 "expected Property Type Mismatch error, got: {:?}",
2833 result.errors
2834 );
2835 }
2836
2837 #[tokio::test]
2838 async fn unknown_property_emits_error() {
2839 let result = typecheck("Frame { Bogus = 1; }").await;
2840 assert!(
2841 has_unknown_property_error(&result),
2842 "expected Unknown Property error, got: {:?}",
2843 result.errors
2844 );
2845 }
2846
2847 #[tokio::test]
2848 async fn multi_class_nonstrict_accepts_partial_property() {
2849 let result = typecheck("TextButton, Frame { Text = \"hi\"; }").await;
2850 assert!(
2851 !has_unknown_property_error(&result),
2852 "unexpected Unknown Property error in nonstrict mode, got: {:?}",
2853 result.errors
2854 );
2855 }
2856
2857 #[tokio::test]
2858 async fn multi_class_strict_directive_rejects_partial_property() {
2859 let result = typecheck("--!strict\nTextButton, Frame { Text = \"hi\"; }").await;
2860 assert!(
2861 has_unknown_property_error(&result),
2862 "expected Unknown Property error in strict mode, got: {:?}",
2863 result.errors
2864 );
2865 }
2866
2867 #[tokio::test]
2868 async fn luaurc_strict_rejects_partial_property() {
2869 let result = typecheck_with_luaurc(
2870 "TextButton, Frame { Text = \"hi\"; }",
2871 Some(r#"{ "languageMode": "strict" }"#),
2872 )
2873 .await;
2874 assert!(
2875 has_unknown_property_error(&result),
2876 "expected Unknown Property error from luaurc strict mode, got: {:?}",
2877 result.errors
2878 );
2879 }
2880
2881 #[tokio::test]
2882 async fn directive_nonstrict_overrides_luaurc_strict() {
2883 let result = typecheck_with_luaurc(
2884 "--!nonstrict\nTextButton, Frame { Text = \"hi\"; }",
2885 Some(r#"{ "languageMode": "strict" }"#),
2886 )
2887 .await;
2888 assert!(
2889 !has_unknown_property_error(&result),
2890 "unexpected Unknown Property error when directive overrides luaurc, got: {:?}",
2891 result.errors
2892 );
2893 }
2894
2895 #[tokio::test]
2896 async fn luaurc_unknown_mode_treated_as_nonstrict() {
2897 let result = typecheck_with_luaurc(
2898 "TextButton, Frame { Text = \"hi\"; }",
2899 Some(r#"{ "languageMode": "nocheck" }"#),
2900 )
2901 .await;
2902 assert!(
2903 !has_unknown_property_error(&result),
2904 "unexpected Unknown Property error for unknown luaurc mode, got: {:?}",
2905 result.errors
2906 );
2907 }
2908
2909 #[tokio::test]
2910 async fn multi_class_unknown_everywhere_errors_in_nonstrict() {
2911 let result = typecheck("TextButton, Frame { Bogus = 1; }").await;
2912 assert!(
2913 has_unknown_property_error(&result),
2914 "expected Unknown Property error when property missing on every class, got: {:?}",
2915 result.errors
2916 );
2917 }
2918
2919 #[tokio::test]
2920 async fn multi_class_shared_property_no_error() {
2921 let result =
2922 typecheck("Frame, TextLabel { BackgroundColor3 = Color3.new(1, 1, 1); }").await;
2923 assert!(
2924 !has_unknown_property_error(&result) && !has_property_type_mismatch_error(&result),
2925 "unexpected property diagnostics for shared property, got: {:?}",
2926 result.errors
2927 );
2928 }
2929
2930 #[tokio::test]
2931 async fn pseudo_selector_skips_property_check() {
2932 let result = typecheck("UICorner { CornerRadius = UDim.new(0, 8); }").await;
2933 assert!(
2934 !has_unknown_property_error(&result) && !has_property_type_mismatch_error(&result),
2935 "unexpected property diagnostics in pseudo-selector body, got: {:?}",
2936 result.errors
2937 );
2938 }
2939}