Skip to main content

rbx_rsml/typechecker/
mod.rs

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
127/// Tokens like `StateSelectorOrEnumPart` and `TagSelectorOrEnumPart` span the
128/// leading `:` or `.` sigil alongside the identifier. Diagnostics that point
129/// at just the name should skip that single byte prefix.
130fn 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        // A separate `AstErrors` is needed because the shared one would conflict
206        // with borrows of `self` taken further down.
207        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        // Validate any enum references on the RHS. If invalid, the LHS type
385        // collapses to `unknown`.
386        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    /// Cross-checks a property assignment against the reflection database.
479    /// Emits `UnknownProperty` when the property doesn't appear on the selector
480    /// classes, and `PropertyTypeMismatch` when the RHS's runtime type doesn't
481    /// match the declared type. Skipped when `current_classes` is empty — that
482    /// covers global-scope assignments and pseudo-selector bodies
483    /// (`UICorner { ... }`) where no Instance class drives the lookup.
484    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        // Multi-class selectors with differing declared types are essentially
541        // nonexistent in Roblox — compare against the first class that declares
542        // the property.
543        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    /// Validates every enum reference on the RHS of an assignment against the
566    /// reflection DB. The `left` node supplies the implicit enum name used by
567    /// a top-level shorthand form (`:Variant`) — its name is rebinded via
568    /// [`shorthand_rebind`] to match runtime evaluator behavior. Returns
569    /// `false` when any error was pushed.
570    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        // Top-level shorthand `:Variant` — derive enum name from the LHS.
579        '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        // Outer scope gets split by inner scope insertion, so 3 entries:
1206        // two halves of the outer Frame scope + the inner TextButton scope
1207        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}