selene_lib/lints/
standard_library.rs

1use super::{super::standard_library::*, *};
2use crate::{
3    ast_util::{name_paths::*, scopes::ScopeManager},
4    possible_std::possible_standard_library_notes,
5};
6use std::convert::Infallible;
7
8use full_moon::{
9    ast::{self, Ast, Expression},
10    node::Node,
11    tokenizer::{Position, Symbol, TokenType},
12    visitors::Visitor,
13};
14
15pub struct StandardLibraryLint;
16
17impl Lint for StandardLibraryLint {
18    type Config = ();
19    type Error = Infallible;
20
21    const SEVERITY: Severity = Severity::Error;
22    const LINT_TYPE: LintType = LintType::Correctness;
23
24    fn new(_: Self::Config) -> Result<Self, Self::Error> {
25        Ok(StandardLibraryLint)
26    }
27
28    fn pass(&self, ast: &Ast, context: &Context, ast_context: &AstContext) -> Vec<Diagnostic> {
29        let mut visitor = StandardLibraryVisitor {
30            diagnostics: Vec::new(),
31            scope_manager: &ast_context.scope_manager,
32            standard_library: &context.standard_library,
33            user_set_standard_library: &context.user_set_standard_library,
34        };
35
36        visitor.visit_ast(ast);
37
38        visitor.diagnostics
39    }
40}
41
42fn same_type_if_equal(lhs: &Expression, rhs: &Expression) -> Option<PassedArgumentType> {
43    let lhs_type = get_argument_type(lhs);
44    let rhs_type = get_argument_type(rhs);
45
46    if lhs_type == rhs_type {
47        lhs_type
48    } else {
49        None
50    }
51}
52
53// Returns the argument type of the expression if it can be constantly resolved
54// Otherwise, returns None
55// Only attempts to resolve constants
56fn get_argument_type(expression: &ast::Expression) -> Option<PassedArgumentType> {
57    #[cfg_attr(
58        feature = "force_exhaustive_checks",
59        deny(non_exhaustive_omitted_patterns)
60    )]
61    match expression {
62        ast::Expression::Parentheses { expression, .. } => get_argument_type(expression),
63
64        ast::Expression::UnaryOperator { unop, expression } => {
65            match unop {
66                // CAVEAT: If you're overriding __len on a userdata and then making it not return a number
67                // ...sorry, but I don't care about your code :)
68                ast::UnOp::Hash(_) => Some(ArgumentType::Number.into()),
69                ast::UnOp::Minus(_) => get_argument_type(expression),
70                ast::UnOp::Not(_) => Some(ArgumentType::Bool.into()),
71                #[cfg(feature = "lua53")]
72                ast::UnOp::Tilde(_) => get_argument_type(expression),
73                _ => None,
74            }
75        }
76
77        ast::Expression::Function(_) => Some(ArgumentType::Function.into()),
78        ast::Expression::FunctionCall(_) => None,
79        ast::Expression::Number(_) => Some(ArgumentType::Number.into()),
80        ast::Expression::String(token) => {
81            Some(PassedArgumentType::from_string(token.token().to_string()))
82        }
83        #[cfg_attr(
84            feature = "force_exhaustive_checks",
85            allow(non_exhaustive_omitted_patterns)
86        )]
87        ast::Expression::Symbol(symbol) => match *symbol.token_type() {
88            TokenType::Symbol { symbol } => match symbol {
89                Symbol::False => Some(ArgumentType::Bool.into()),
90                Symbol::True => Some(ArgumentType::Bool.into()),
91                Symbol::Nil => Some(ArgumentType::Nil.into()),
92                Symbol::Ellipsis => Some(ArgumentType::Vararg.into()),
93                ref other => {
94                    unreachable!("TokenType::Symbol was not expected ({:?})", other)
95                }
96            },
97
98            ref other => unreachable!(
99                "ast::Expression::Symbol token_type != TokenType::Symbol ({:?})",
100                other
101            ),
102        },
103        ast::Expression::TableConstructor(_) => Some(ArgumentType::Table.into()),
104        ast::Expression::Var(_) => None,
105
106        #[cfg(feature = "roblox")]
107        ast::Expression::IfExpression(if_expression) => {
108            // This could be a union type
109            let expected_type = get_argument_type(if_expression.if_expression())?;
110
111            if let Some(else_if_expressions) = if_expression.else_if_expressions() {
112                for else_if_expression in else_if_expressions {
113                    if !get_argument_type(else_if_expression.expression())?
114                        .same_type(&expected_type)
115                    {
116                        return None;
117                    }
118                }
119            }
120
121            get_argument_type(if_expression.else_expression())?
122                .same_type(&expected_type)
123                .then_some(expected_type)
124        }
125
126        #[cfg(feature = "roblox")]
127        ast::Expression::InterpolatedString(interpolated_string) => {
128            if interpolated_string.expressions().next().is_some() {
129                Some(ArgumentType::String.into())
130            } else {
131                // Simple string, aka `Workspace`
132                Some(PassedArgumentType::from_string(
133                    interpolated_string.last_string().token().to_string(),
134                ))
135            }
136        }
137
138        ast::Expression::BinaryOperator {
139            lhs, binop, rhs, ..
140        } => {
141            // Nearly all of these will return wrong results if you have a non-idiomatic metatable
142            // I intentionally omitted common metamethod re-typings, like __mul
143            match binop {
144                ast::BinOp::Caret(_) => Some(ArgumentType::Number.into()),
145
146                #[cfg_attr(
147                    feature = "force_exhaustive_checks",
148                    allow(non_exhaustive_omitted_patterns)
149                )]
150                ast::BinOp::GreaterThan(_)
151                | ast::BinOp::GreaterThanEqual(_)
152                | ast::BinOp::LessThan(_)
153                | ast::BinOp::LessThanEqual(_)
154                | ast::BinOp::TwoEqual(_)
155                | ast::BinOp::TildeEqual(_) => {
156                    if_chain::if_chain! {
157                        if let ast::Expression::BinaryOperator { binop, .. } = &**rhs;
158                        if let ast::BinOp::And(_) | ast::BinOp::Or(_) = binop;
159                        then {
160                            None
161                        } else {
162                            Some(ArgumentType::Bool.into())
163                        }
164                    }
165                }
166
167                // Basic types will often re-implement these (e.g. Roblox's Vector3)
168                ast::BinOp::Plus(_)
169                | ast::BinOp::Minus(_)
170                | ast::BinOp::Star(_)
171                | ast::BinOp::Slash(_) => same_type_if_equal(lhs, rhs),
172
173                #[cfg(feature = "lua53")]
174                ast::BinOp::DoubleLessThan(_)
175                | ast::BinOp::DoubleGreaterThan(_)
176                | ast::BinOp::Ampersand(_)
177                | ast::BinOp::Tilde(_)
178                | ast::BinOp::Pipe(_) => same_type_if_equal(lhs, rhs),
179
180                #[cfg(any(feature = "lua53", feature = "roblox"))]
181                ast::BinOp::DoubleSlash(_) => same_type_if_equal(lhs, rhs),
182
183                ast::BinOp::Percent(_) => Some(ArgumentType::Number.into()),
184
185                ast::BinOp::TwoDots(_) => Some(ArgumentType::String.into()),
186
187                ast::BinOp::And(_) | ast::BinOp::Or(_) => {
188                    // We could potentially support union types here
189                    // Or even just produce one type if both the left and right sides can be evaluated
190                    // But for now, the evaluation just isn't smart enough to where this would be practical
191                    None
192                }
193
194                _ => None,
195            }
196        }
197
198        #[cfg(feature = "roblox")]
199        ast::Expression::TypeAssertion { expression, .. } => get_argument_type(expression),
200
201        _ => None,
202    }
203}
204
205pub struct StandardLibraryVisitor<'std> {
206    diagnostics: Vec<Diagnostic>,
207    scope_manager: &'std ScopeManager,
208    standard_library: &'std StandardLibrary,
209    user_set_standard_library: &'std Option<Vec<String>>,
210}
211
212impl StandardLibraryVisitor<'_> {
213    fn lint_invalid_field_access(
214        &mut self,
215        mut name_path: Vec<String>,
216        range: (Position, Position),
217    ) {
218        // Make sure it's not just `bad()`, and that it's not a field access from a global outside of standard library
219        if self.standard_library.find_global(&name_path).is_none()
220            && self.standard_library.global_has_fields(&name_path[0])
221        {
222            let field = name_path.pop().unwrap();
223            assert!(!name_path.is_empty(), "name_path is empty");
224
225            // check if it's writable
226            for bound in 1..=name_path.len() {
227                let path = &name_path[0..bound];
228                match self.standard_library.find_global(path) {
229                    Some(field) => {
230                        match field.field_kind {
231                            FieldKind::Any => return,
232
233                            FieldKind::Property(writability) => {
234                                if writability != PropertyWritability::ReadOnly
235                                    && writability != PropertyWritability::OverrideFields
236                                {
237                                    return;
238                                }
239                            }
240
241                            _ => {}
242                        };
243                    }
244
245                    None => break,
246                }
247            }
248
249            let mut name_path_with_field = name_path.iter().map(String::as_str).collect::<Vec<_>>();
250            name_path_with_field.push(&field);
251
252            self.diagnostics.push(Diagnostic::new_complete(
253                "incorrect_standard_library_use",
254                format!(
255                    "standard library global `{}` does not contain the field `{}`",
256                    name_path.join("."),
257                    field,
258                ),
259                Label::new((range.0.bytes(), range.1.bytes())),
260                possible_standard_library_notes(
261                    &name_path_with_field,
262                    self.user_set_standard_library,
263                ),
264                Vec::new(),
265            ));
266        }
267    }
268}
269
270impl Visitor for StandardLibraryVisitor<'_> {
271    fn visit_assignment(&mut self, assignment: &ast::Assignment) {
272        for var in assignment.variables() {
273            if let Some(reference) = self
274                .scope_manager
275                .reference_at_byte(var.start_position().unwrap().bytes())
276            {
277                if reference.resolved.is_some() {
278                    return;
279                }
280            }
281
282            match var {
283                ast::Var::Expression(var_expr) => {
284                    let mut keep_going = true;
285                    if var_expr
286                        .suffixes()
287                        .take_while(|suffix| take_while_keep_going(suffix, &mut keep_going))
288                        .count()
289                        != var_expr.suffixes().count()
290                    {
291                        // Modifying the return value, which we don't lint yet
292                        continue;
293                    }
294
295                    if let Some(name_path) =
296                        name_path_from_prefix_suffix(var_expr.prefix(), var_expr.suffixes())
297                    {
298                        match self.standard_library.find_global(&name_path) {
299                            Some(field) => {
300                                match field.field_kind {
301                                    FieldKind::Property(writability) => {
302                                        if writability != PropertyWritability::ReadOnly
303                                            && writability != PropertyWritability::NewFields
304                                        {
305                                            continue;
306                                        }
307                                    }
308                                    FieldKind::Any => continue,
309                                    _ => {}
310                                };
311
312                                let range = var_expr.range().unwrap();
313
314                                self.diagnostics.push(Diagnostic::new_complete(
315                                    "incorrect_standard_library_use",
316                                    format!(
317                                        "standard library global `{}` is not writable",
318                                        name_path.join("."),
319                                    ),
320                                    Label::new((range.0.bytes(), range.1.bytes())),
321                                    Vec::new(),
322                                    Vec::new(),
323                                ));
324                            }
325
326                            None => {
327                                self.lint_invalid_field_access(
328                                    name_path,
329                                    var_expr.range().unwrap(),
330                                );
331                            }
332                        }
333                    }
334                }
335
336                ast::Var::Name(name_token) => {
337                    let name = name_token.token().to_string();
338
339                    if let Some(global) = self.standard_library.find_global(&[name.to_owned()]) {
340                        match global.field_kind {
341                            FieldKind::Property(writability) => {
342                                if writability != PropertyWritability::ReadOnly
343                                    && writability != PropertyWritability::NewFields
344                                {
345                                    continue;
346                                }
347                            }
348                            FieldKind::Any => continue,
349                            _ => {}
350                        };
351
352                        let range = name_token.range().unwrap();
353
354                        self.diagnostics.push(Diagnostic::new_complete(
355                            "incorrect_standard_library_use",
356                            format!("standard library global `{name}` is not overridable",),
357                            Label::new((range.0.bytes(), range.1.bytes())),
358                            Vec::new(),
359                            Vec::new(),
360                        ));
361                    }
362                }
363
364                _ => {}
365            }
366        }
367    }
368
369    fn visit_expression(&mut self, expression: &ast::Expression) {
370        if let Some(reference) = self
371            .scope_manager
372            .reference_at_byte(expression.start_position().unwrap().bytes())
373        {
374            if reference.resolved.is_some() {
375                return;
376            }
377        }
378
379        if let Some(name_path) = name_path(expression) {
380            self.lint_invalid_field_access(name_path, expression.range().unwrap());
381        }
382    }
383
384    fn visit_function_call(&mut self, call: &ast::FunctionCall) {
385        if let Some(reference) = self
386            .scope_manager
387            .reference_at_byte(call.start_position().unwrap().bytes())
388        {
389            if reference.resolved.is_some() {
390                return;
391            }
392        }
393
394        let mut keep_going = true;
395        let mut suffixes: Vec<&ast::Suffix> = call
396            .suffixes()
397            .take_while(|suffix| take_while_keep_going(suffix, &mut keep_going))
398            .collect();
399
400        let mut name_path =
401            match name_path_from_prefix_suffix(call.prefix(), suffixes.iter().copied()) {
402                Some(name_path) => name_path,
403                None => return,
404            };
405
406        let call_suffix = suffixes.pop().unwrap();
407
408        let field = match self.standard_library.find_global(&name_path) {
409            Some(field) => field,
410            None => {
411                self.lint_invalid_field_access(
412                    name_path,
413                    (
414                        call.prefix().start_position().unwrap(),
415                        if let ast::Suffix::Call(ast::Call::MethodCall(method_call)) = call_suffix {
416                            method_call.name().end_position().unwrap()
417                        } else {
418                            suffixes
419                                .last()
420                                .and_then(|suffix| suffix.end_position())
421                                .unwrap_or_else(|| call.prefix().end_position().unwrap())
422                        },
423                    ),
424                );
425                return;
426            }
427        };
428
429        let function = match &field.field_kind {
430            FieldKind::Any => return,
431            FieldKind::Function(function) => function,
432            _ => {
433                self.diagnostics.push(Diagnostic::new(
434                    "incorrect_standard_library_use",
435                    format!(
436                        "standard library field `{}` is not a function",
437                        name_path.join("."),
438                    ),
439                    Label::from_node(call, None),
440                ));
441
442                return;
443            }
444        };
445
446        let (function_args, call_is_method) = match call_suffix {
447            ast::Suffix::Call(call) => match call {
448                ast::Call::AnonymousCall(args) => (args, false),
449                ast::Call::MethodCall(method_call) => (method_call.args(), true),
450                _ => return,
451            },
452
453            _ => unreachable!("function_call.call_suffix != ast::Suffix::Call"),
454        };
455
456        if function.method != call_is_method {
457            let problem = if call_is_method {
458                "is not a method"
459            } else {
460                "is a method"
461            };
462
463            let using = if call_is_method { ":" } else { "." };
464            let use_instead = if call_is_method { "." } else { ":" };
465
466            let name = name_path.pop().unwrap();
467
468            self.diagnostics.push(Diagnostic::new_complete(
469                "incorrect_standard_library_use",
470                format!(
471                    "standard library function `{}{}{}` {}",
472                    name_path.join("."),
473                    using,
474                    name,
475                    problem,
476                ),
477                Label::from_node(call, None),
478                vec![format!(
479                    "try: {}{}{}(...)",
480                    name_path.join("."),
481                    use_instead,
482                    name
483                )],
484                Vec::new(),
485            ));
486
487            return;
488        }
489
490        let mut argument_types = Vec::new();
491
492        #[cfg_attr(
493            feature = "force_exhaustive_checks",
494            deny(non_exhaustive_omitted_patterns)
495        )]
496        match function_args {
497            ast::FunctionArgs::Parentheses { arguments, .. } => {
498                for argument in arguments {
499                    argument_types.push((argument.range().unwrap(), get_argument_type(argument)));
500                }
501            }
502
503            ast::FunctionArgs::String(token) => {
504                argument_types.push((
505                    token.range().unwrap(),
506                    Some(PassedArgumentType::from_string(token.token().to_string())),
507                ));
508            }
509
510            ast::FunctionArgs::TableConstructor(table) => {
511                argument_types.push((table.range().unwrap(), Some(ArgumentType::Table.into())));
512            }
513
514            _ => {}
515        }
516
517        let mut expected_args = function
518            .arguments
519            .iter()
520            .filter(|arg| arg.required != Required::NotRequired)
521            .count();
522
523        let mut vararg = false;
524        let mut max_args = function.arguments.len();
525
526        let mut maybe_more_arguments = false;
527
528        if let ast::FunctionArgs::Parentheses { arguments, .. } = function_args {
529            if let Some(ast::punctuated::Pair::End(last_argument)) = arguments.last() {
530                match last_argument {
531                    ast::Expression::FunctionCall(_) => {
532                        maybe_more_arguments = true;
533                    }
534
535                    ast::Expression::Symbol(token_ref) => {
536                        if let TokenType::Symbol { symbol } = token_ref.token().token_type() {
537                            if symbol == &full_moon::tokenizer::Symbol::Ellipsis {
538                                maybe_more_arguments = true;
539                            }
540                        }
541                    }
542
543                    _ => {}
544                }
545            }
546        };
547
548        if let Some(last) = function.arguments.last() {
549            if last.argument_type == ArgumentType::Vararg {
550                if let Required::Required(message) = &last.required {
551                    // Functions like math.ceil where not using the vararg is wrong
552                    if function.arguments.len() > argument_types.len() && !maybe_more_arguments {
553                        self.diagnostics.push(Diagnostic::new_complete(
554                            "incorrect_standard_library_use",
555                            format!(
556                                // TODO: This message isn't great
557                                "standard library function `{}` requires use of the vararg",
558                                name_path.join("."),
559                            ),
560                            Label::from_node(call, None),
561                            message.iter().cloned().collect(),
562                            Vec::new(),
563                        ));
564                    }
565
566                    expected_args -= 1;
567                    max_args -= 1;
568                }
569
570                vararg = true;
571            }
572        }
573
574        let arguments_length = argument_types.len();
575
576        if (arguments_length < expected_args && !maybe_more_arguments)
577            || (!vararg && arguments_length > max_args)
578        {
579            let required_param_message = function
580                .arguments
581                .get(arguments_length)
582                .into_iter()
583                .filter_map(|arg| match &arg.required {
584                    Required::Required(Some(message)) => Some(message.clone()),
585                    _ => None,
586                })
587                .collect();
588
589            self.diagnostics.push(Diagnostic::new_complete(
590                "incorrect_standard_library_use",
591                format!(
592                    "standard library function `{}` requires {} parameters, {} passed",
593                    name_path.join("."),
594                    expected_args,
595                    argument_types.len(),
596                ),
597                Label::from_node(call, None),
598                required_param_message,
599                Vec::new(),
600            ));
601        }
602
603        for ((range, passed_type), expected) in argument_types.iter().zip(function.arguments.iter())
604        {
605            if expected.argument_type == ArgumentType::Vararg {
606                continue;
607            }
608
609            if let Some(passed_type) = passed_type {
610                // Allow nil for unrequired arguments
611                if expected.required == Required::NotRequired
612                    && passed_type == &PassedArgumentType::Primitive(ArgumentType::Nil)
613                {
614                    continue;
615                }
616
617                let matches = passed_type.matches(&expected.argument_type);
618
619                if !matches {
620                    self.diagnostics.push(Diagnostic::new(
621                        "incorrect_standard_library_use",
622                        format!(
623                            "use of standard_library function `{}` is incorrect",
624                            name_path.join("."),
625                        ),
626                        Label::new_with_message(
627                            (range.0.bytes() as u32, range.1.bytes() as u32),
628                            format!(
629                                "expected `{}`, received `{}`",
630                                expected.argument_type,
631                                passed_type.type_name()
632                            ),
633                        ),
634                    ));
635                }
636            }
637        }
638    }
639}
640
641#[derive(Debug, PartialEq, Eq)]
642enum PassedArgumentType {
643    Primitive(ArgumentType),
644    String(String),
645}
646
647impl PassedArgumentType {
648    fn from_string(mut string: String) -> PassedArgumentType {
649        string.pop();
650        PassedArgumentType::String(string.chars().skip(1).collect())
651    }
652
653    fn matches(&self, argument_type: &ArgumentType) -> bool {
654        if argument_type == &ArgumentType::Any {
655            return true;
656        }
657
658        match self {
659            PassedArgumentType::Primitive(us) => {
660                us == &ArgumentType::Vararg
661                    || us == argument_type
662                    || (us == &ArgumentType::String
663                        && matches!(argument_type, ArgumentType::Constant(_)))
664            }
665            PassedArgumentType::String(text) => match argument_type {
666                ArgumentType::Constant(constants) => constants.contains(text),
667                ArgumentType::String => true,
668                _ => false,
669            },
670        }
671    }
672
673    // Roblox feature flag uses this, and I don't want to lock it
674    #[allow(dead_code)]
675    fn same_type(&self, other: &PassedArgumentType) -> bool {
676        match (self, other) {
677            (PassedArgumentType::Primitive(a), PassedArgumentType::Primitive(b)) => a == b,
678            (PassedArgumentType::String(_), PassedArgumentType::String(_)) => true,
679            _ => false,
680        }
681    }
682
683    fn type_name(&self) -> String {
684        match self {
685            PassedArgumentType::Primitive(argument_type) => argument_type.to_string(),
686            PassedArgumentType::String(_) => ArgumentType::String.to_string(),
687        }
688    }
689}
690
691impl From<ArgumentType> for PassedArgumentType {
692    fn from(argument_type: ArgumentType) -> Self {
693        PassedArgumentType::Primitive(argument_type)
694    }
695}
696
697#[cfg(test)]
698mod tests {
699    use super::{super::test_util::*, *};
700
701    #[test]
702    fn test_name_path() {
703        let ast = full_moon::parse("local x = foo; local y = foo.bar.baz").unwrap();
704
705        struct NamePathTestVisitor {
706            paths: Vec<Vec<String>>,
707        }
708
709        impl Visitor for NamePathTestVisitor {
710            fn visit_local_assignment(&mut self, node: &ast::LocalAssignment) {
711                self.paths.push(
712                    name_path(node.expressions().into_iter().next().unwrap())
713                        .expect("name_path returned None"),
714                );
715            }
716        }
717
718        let mut visitor = NamePathTestVisitor { paths: Vec::new() };
719
720        visitor.visit_ast(&ast);
721
722        assert_eq!(
723            visitor.paths,
724            vec![
725                vec!["foo".to_owned()],
726                vec!["foo".to_owned(), "bar".to_owned(), "baz".to_owned()],
727            ]
728        );
729    }
730
731    #[test]
732    fn test_any() {
733        test_lint(
734            StandardLibraryLint::new(()).unwrap(),
735            "standard_library",
736            "any",
737        );
738    }
739
740    #[test]
741    fn test_assert() {
742        test_lint(
743            StandardLibraryLint::new(()).unwrap(),
744            "standard_library",
745            "assert",
746        );
747    }
748
749    #[test]
750    fn test_bad_call_signatures() {
751        test_lint(
752            StandardLibraryLint::new(()).unwrap(),
753            "standard_library",
754            "bad_call_signatures",
755        );
756    }
757
758    #[test]
759    fn test_callable_metatables() {
760        test_lint(
761            StandardLibraryLint::new(()).unwrap(),
762            "standard_library",
763            "callable_metatables",
764        );
765    }
766
767    #[test]
768    fn test_complex() {
769        test_lint(
770            StandardLibraryLint::new(()).unwrap(),
771            "standard_library",
772            "complex",
773        );
774    }
775
776    #[test]
777    fn test_constants() {
778        test_lint(
779            StandardLibraryLint::new(()).unwrap(),
780            "standard_library",
781            "constants",
782        );
783    }
784
785    #[test]
786    fn test_lua52() {
787        test_lint_config(
788            StandardLibraryLint::new(()).unwrap(),
789            "standard_library",
790            "lua52",
791            TestUtilConfig {
792                standard_library: StandardLibrary::from_name("lua52").unwrap(),
793                ..TestUtilConfig::default()
794            },
795        );
796    }
797
798    #[test]
799    fn test_math_on_types() {
800        test_lint(
801            StandardLibraryLint::new(()).unwrap(),
802            "standard_library",
803            "math_on_types",
804        );
805    }
806
807    #[test]
808    fn test_method_call() {
809        test_lint(
810            StandardLibraryLint::new(()).unwrap(),
811            "standard_library",
812            "method_call",
813        );
814    }
815
816    #[test]
817    fn test_required() {
818        test_lint(
819            StandardLibraryLint::new(()).unwrap(),
820            "standard_library",
821            "required",
822        );
823    }
824
825    #[test]
826    fn test_shadowing() {
827        test_lint(
828            StandardLibraryLint::new(()).unwrap(),
829            "standard_library",
830            "shadowing",
831        );
832    }
833
834    #[test]
835    fn test_ternary() {
836        test_lint(
837            StandardLibraryLint::new(()).unwrap(),
838            "standard_library",
839            "ternary",
840        );
841    }
842
843    #[test]
844    fn test_unknown_property() {
845        test_lint(
846            StandardLibraryLint::new(()).unwrap(),
847            "standard_library",
848            "unknown_property",
849        );
850    }
851
852    #[test]
853    fn test_unpack_function_arguments() {
854        test_lint(
855            StandardLibraryLint::new(()).unwrap(),
856            "standard_library",
857            "unpack_function_arguments",
858        );
859    }
860
861    #[test]
862    fn test_vararg() {
863        test_lint(
864            StandardLibraryLint::new(()).unwrap(),
865            "standard_library",
866            "vararg",
867        );
868    }
869
870    #[test]
871    fn test_wildcard() {
872        test_lint_config_with_output(
873            StandardLibraryLint::new(()).unwrap(),
874            "standard_library",
875            "wildcard",
876            TestUtilConfig::default(),
877            if cfg!(feature = "roblox") {
878                "stderr"
879            } else {
880                "noroblox.stderr"
881            },
882        );
883    }
884
885    #[test]
886    fn test_wildcard_structs() {
887        test_lint_config_with_output(
888            StandardLibraryLint::new(()).unwrap(),
889            "standard_library",
890            "wildcard_structs",
891            TestUtilConfig::default(),
892            if cfg!(feature = "roblox") {
893                "stderr"
894            } else {
895                "noroblox.stderr"
896            },
897        );
898    }
899
900    #[test]
901    fn test_writing() {
902        test_lint(
903            StandardLibraryLint::new(()).unwrap(),
904            "standard_library",
905            "writing",
906        );
907    }
908
909    #[cfg(feature = "roblox")]
910    #[test]
911    fn test_if_expressions() {
912        test_lint(
913            StandardLibraryLint::new(()).unwrap(),
914            "standard_library",
915            "if_expressions",
916        );
917    }
918
919    #[cfg(feature = "roblox")]
920    #[test]
921    fn test_string_interpolation() {
922        test_lint(
923            StandardLibraryLint::new(()).unwrap(),
924            "standard_library",
925            "string_interpolation",
926        );
927    }
928}