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
53fn 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 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 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 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 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 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 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 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 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 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 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 "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 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 #[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}