diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index c4a8fdffb..e6dfe2d82 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -119,6 +119,7 @@ mod jest { pub mod no_test_prefixes; pub mod valid_describe_callback; pub mod valid_expect; + pub mod valid_title; } mod unicorn { @@ -224,6 +225,7 @@ oxc_macros::declare_all_lint_rules! { jest::no_export, jest::no_standalone_expect, jest::no_identical_title, + jest::valid_title, unicorn::no_instanceof_array, unicorn::no_unnecessary_await, unicorn::no_thenable, diff --git a/crates/oxc_linter/src/rules/jest/valid_title.rs b/crates/oxc_linter/src/rules/jest/valid_title.rs new file mode 100644 index 000000000..b6d374f83 --- /dev/null +++ b/crates/oxc_linter/src/rules/jest/valid_title.rs @@ -0,0 +1,865 @@ +use std::{collections::HashMap, hash::Hash}; + +use oxc_ast::{ + ast::{Argument, BinaryExpression, Expression}, + AstKind, +}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::{Atom, GetSpan, Span}; +use regex::Regex; + +use crate::{ + context::LintContext, + jest_ast_util::{parse_general_jest_fn_call, JestFnKind, JestGeneralFnKind}, + rule::Rule, + AstNode, +}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint(jest/valid-title): {0:?}")] +#[diagnostic(severity(warning), help("{1:?}"))] +struct ValidTitleDiagnostic(Atom, &'static str, #[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct ValidTitle { + ignore_type_of_describe_name: bool, + disallowed_words: Vec, + ignore_space: bool, + must_not_match_patterns: HashMap, + must_match_patterns: HashMap, +} + +declare_oxc_lint!( + /// ### What it does + /// + /// Checks that the title of Jest blocks are valid by ensuring that titles are: + /// + /// - not empty, + /// - is a string, + /// - not prefixed with their block name, + /// - have no leading or trailing spaces + /// + /// ### Example + /// ```javascript + /// describe('', () => {}); + /// describe('foo', () => { + /// it('', () => {}); + /// }); + /// it('', () => {}); + /// test('', () => {}); + /// xdescribe('', () => {}); + /// xit('', () => {}); + /// xtest('', () => {}); + /// ``` + ValidTitle, + restriction +); + +impl Rule for ValidTitle { + fn from_configuration(value: serde_json::Value) -> Self { + let config = value.get(0); + let get_as_bool = |name: &str| -> bool { + config + .and_then(|v| v.get(name)) + .and_then(serde_json::Value::as_bool) + .unwrap_or_default() + }; + + let ignore_type_of_describe_name = get_as_bool("ignoreTypeOfDescribeName"); + let ignore_space = get_as_bool("ignoreSpaces"); + let disallowed_words = config + .and_then(|v| v.get("disallowedWords")) + .and_then(|v| v.as_array()) + .map(|v| { + v.iter().filter_map(|v| v.as_str().map(std::string::ToString::to_string)).collect() + }) + .unwrap_or_default(); + let must_not_match_patterns = config + .and_then(|v| v.get("mustNotMatch")) + .and_then(compile_matcher_patterns) + .unwrap_or_default(); + let must_match_patterns = config + .and_then(|v| v.get("mustMatch")) + .and_then(compile_matcher_patterns) + .unwrap_or_default(); + Self { + ignore_type_of_describe_name, + disallowed_words, + ignore_space, + must_not_match_patterns, + must_match_patterns, + } + } + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::CallExpression(call_expr) = node.kind() else { + return; + }; + let Some(jest_fn_call) = parse_general_jest_fn_call(call_expr, node, ctx) else { + return; + }; + + if !matches!( + jest_fn_call.kind, + JestFnKind::General(JestGeneralFnKind::Describe | JestGeneralFnKind::Test) + ) { + return; + } + + let Some(Argument::Expression(expr)) = call_expr.arguments.get(0) else { + return; + }; + + let need_report_describe_name = !(self.ignore_type_of_describe_name + && matches!(jest_fn_call.kind, JestFnKind::General(JestGeneralFnKind::Describe))); + + match expr { + Expression::StringLiteral(string_literal) => { + validate_title( + &string_literal.value, + string_literal.span, + self, + &jest_fn_call.name, + ctx, + ); + } + Expression::TemplateLiteral(template_literal) => { + if !template_literal.is_no_substitution_template() { + return; + } + if let Some(quasi) = template_literal.quasi() { + validate_title( + quasi.as_str(), + template_literal.span, + self, + &jest_fn_call.name, + ctx, + ); + } + } + Expression::BinaryExpression(binary_expr) => { + if does_binary_expression_contain_string_node(binary_expr) { + return; + } + if need_report_describe_name { + Message::TitleMustBeString.diagnostic(ctx, expr.span()); + } + } + _ => { + if need_report_describe_name { + Message::TitleMustBeString.diagnostic(ctx, expr.span()); + } + } + } + } +} + +type CompiledMatcherAndMessage = (Regex, Option); + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +enum MatchKind { + Describe, + It, + Test, +} + +#[derive(Copy, Clone)] +enum MatcherPattern<'a> { + String(&'a serde_json::Value), + Vec(&'a Vec), +} + +impl MatchKind { + fn from(name: &str) -> Option { + match name { + "describe" => Some(Self::Describe), + "it" => Some(Self::It), + "test" => Some(Self::Test), + _ => None, + } + } +} + +fn compile_matcher_patterns( + matcher_patterns: &serde_json::Value, +) -> Option> { + matcher_patterns + .as_array() + .map_or_else( + || { + // for `{ "describe": "/pattern/" }` + let obj = matcher_patterns.as_object()?; + let mut map: HashMap = HashMap::new(); + for (key, value) in obj { + let Some(v) = compile_matcher_pattern(MatcherPattern::String(value)) else { + continue; + }; + if let Some(kind) = MatchKind::from(key) { + map.insert(kind, v); + } + } + + Some(map) + }, + |value| { + // for `["/pattern/", "message"]` + let mut map: HashMap = HashMap::new(); + let v = &compile_matcher_pattern(MatcherPattern::Vec(value))?; + map.insert(MatchKind::Describe, v.clone()); + map.insert(MatchKind::Test, v.clone()); + map.insert(MatchKind::It, v.clone()); + Some(map) + }, + ) + .map_or_else( + || { + // for `"/pattern/"` + let string = matcher_patterns.as_str()?; + let mut map: HashMap = HashMap::new(); + let v = &compile_matcher_pattern(MatcherPattern::String( + &serde_json::Value::String(string.to_string()), + ))?; + map.insert(MatchKind::Describe, v.clone()); + map.insert(MatchKind::Test, v.clone()); + map.insert(MatchKind::It, v.clone()); + Some(map) + }, + Some, + ) +} + +fn compile_matcher_pattern(pattern: MatcherPattern) -> Option { + match pattern { + MatcherPattern::String(pattern) => { + let reg_str = format!("(?u){}", pattern.as_str()?); + let reg = Regex::new(®_str).ok()?; + Some((reg, None)) + } + MatcherPattern::Vec(pattern) => { + let reg_str = pattern.get(0).and_then(|v| v.as_str()).map(|v| format!("(?u){v}"))?; + let reg = Regex::new(®_str).ok()?; + let message = pattern.get(1).map(std::string::ToString::to_string); + Some((reg, message)) + } + } +} + +fn validate_title( + title: &str, + span: Span, + valid_title: &ValidTitle, + name: &str, + ctx: &LintContext, +) { + if title.is_empty() { + Message::EmptyTitle.diagnostic(ctx, span); + } + + if !valid_title.disallowed_words.is_empty() { + let Ok(disallowed_words_reg) = regex::Regex::new(&format!( + r#"(?iu)\b(?:{})\b"#, + valid_title.disallowed_words.join("|").replace('.', r"\.") + )) else { + return; + }; + + if let Some(matched) = disallowed_words_reg.find(title) { + let error = format!("{} is not allowed in test title", matched.as_str()); + ctx.diagnostic(ValidTitleDiagnostic( + Atom::from(error), + "It is included in the `disallowedWords` of your config file, try to remove it from your title", + span, + )); + } + return; + } + + // TODO: support fixer + if !valid_title.ignore_space && title.trim() != title { + Message::AccidentalSpace.diagnostic(ctx, span); + } + + let un_prefixed_name = name.trim_start_matches(['f', 'x']); + let Some(first_word) = title.split(' ').next() else { + return; + }; + + // TODO: support fixer + if first_word == un_prefixed_name { + Message::DuplicatePrefix.diagnostic(ctx, span); + return; + } + + let Some(jest_fn_name) = MatchKind::from(un_prefixed_name) else { + return; + }; + + if let Some((regex, message)) = valid_title.must_match_patterns.get(&jest_fn_name) { + if !regex.is_match(title) { + let raw_pattern = regex.as_str(); + let message = message.as_ref().map_or_else( + || Atom::from(format!("{un_prefixed_name} should match {raw_pattern}")), + |message| Atom::from(message.as_str()), + ); + ctx.diagnostic(ValidTitleDiagnostic( + message, + "Make sure the title matches the `mustMatch` of your config file", + span, + )); + } + } + + if let Some((regex, message)) = valid_title.must_not_match_patterns.get(&jest_fn_name) { + if regex.is_match(title) { + let raw_pattern = regex.as_str(); + let message = message.as_ref().map_or_else( + || Atom::from(format!("{un_prefixed_name} should not match {raw_pattern}")), + |message| Atom::from(message.as_str()), + ); + + ctx.diagnostic(ValidTitleDiagnostic( + message, + "Make sure the title not matches the `mustNotMatch` of your config file", + span, + )); + } + } +} + +fn does_binary_expression_contain_string_node(expr: &BinaryExpression) -> bool { + if expr.left.is_string_literal() || expr.right.is_string_literal() { + return true; + } + + match &expr.left { + Expression::BinaryExpression(left) => does_binary_expression_contain_string_node(left), + _ => false, + } +} + +enum Message { + TitleMustBeString, + EmptyTitle, + DuplicatePrefix, + AccidentalSpace, +} + +impl Message { + fn detail(&self) -> (&'static str, &'static str) { + match self { + Self::TitleMustBeString => ("Title must be a string", "Replace your title with a string"), + Self::EmptyTitle => ("Should not have an empty title", "Write a meaningful title for your test"), + Self::DuplicatePrefix => ("Should not have duplicate prefix", "The function name has already contains the prefix, try remove the duplicate prefix"), + Self::AccidentalSpace => ("Should not have leading or trailing spaces", "Remove the leading or trailing spaces"), + } + } + fn diagnostic(&self, ctx: &LintContext, span: Span) { + let (error, help) = self.detail(); + ctx.diagnostic(ValidTitleDiagnostic(Atom::from(error), help, span)); + } +} + +#[allow(clippy::too_many_lines)] +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("describe('the correct way to properly handle all the things', () => {});", None), + ("test('that all is as it should be', () => {});", None), + ( + "it('correctly sets the value', () => {});", + Some(serde_json::json!([ + { "ignoreTypeOfDescribeName": false, "disallowedWords": ["correct"] }, + ])), + ), + ("it('correctly sets the value', () => {});", Some(serde_json::json!([]))), + ("describe('the correct way to properly handle all the things', () => {});", None), + ("test('that all is as it should be', () => {});", None), + ( + "it('correctly sets the value', () => {});", + Some(serde_json::json!([{ "mustMatch": {} }])), + ), + ( + "it('correctly sets the value', () => {});", + Some(serde_json::json!([{ "mustMatch": " " }])), + ), + ( + "it('correctly sets the value', () => {});", + Some(serde_json::json!([{ "mustMatch": [" "] }])), + ), + ( + "it('correctly sets the value #unit', () => {});", + Some(serde_json::json!([{ "mustMatch": "#(?:unit|integration|e2e)" }])), + ), + ( + "it('correctly sets the value', () => {});", + Some(serde_json::json!([{ "mustMatch": "^[^#]+$|(?:#(?:unit|e2e))" }])), + ), + ( + "it('correctly sets the value', () => {});", + Some(serde_json::json!([{ "mustMatch": { "test": "#(?:unit|integration|e2e)" } }])), + ), + ( + " + describe('things to test', () => { + describe('unit tests #unit', () => { + it('is true', () => { + expect(true).toBe(true); + }); + }); + + describe('e2e tests #e2e', () => { + it('is another test #jest4life', () => {}); + }); + }); + ", + Some(serde_json::json!([{ "mustMatch": { "test": "^[^#]+$|(?:#(?:unit|e2e))" } }])), + ), + ("it('is a string', () => {});", None), + ("it('is' + ' a ' + ' string', () => {});", None), + ("it(1 + ' + ' + 1, () => {});", None), + ("test('is a string', () => {});", None), + ("xtest('is a string', () => {});", None), + ("xtest(`${myFunc} is a string`, () => {});", None), + ("describe('is a string', () => {});", None), + ("describe.skip('is a string', () => {});", None), + ("describe.skip(`${myFunc} is a string`, () => {});", None), + ("fdescribe('is a string', () => {});", None), + ( + "describe(String(/.+/), () => {});", + Some(serde_json::json!([{ "ignoreTypeOfDescribeName": true }])), + ), + ( + "describe(myFunction, () => {});", + Some(serde_json::json!([{ "ignoreTypeOfDescribeName": true }])), + ), + ( + "xdescribe(skipFunction, () => {});", + Some(serde_json::json!([{ "ignoreTypeOfDescribeName": true, "disallowedWords": [] }])), + ), + ("describe()", None), + ("someFn('', function () {})", None), + ("describe('foo', function () {})", None), + ("describe('foo', function () { it('bar', function () {}) })", None), + ("test('foo', function () {})", None), + ("test.concurrent('foo', function () {})", None), + ("test(`foo`, function () {})", None), + ("test.concurrent(`foo`, function () {})", None), + ("test(`${foo}`, function () {})", None), + ("test.concurrent(`${foo}`, function () {})", None), + ("it('foo', function () {})", None), + ("it.each([])()", None), + ("it.concurrent('foo', function () {})", None), + ("xdescribe('foo', function () {})", None), + ("xit('foo', function () {})", None), + ("xtest('foo', function () {})", None), + ("it()", None), + ("it.concurrent()", None), + ("describe()", None), + ("it.each()()", None), + ("describe('foo', function () {})", None), + ("fdescribe('foo', function () {})", None), + ("xdescribe('foo', function () {})", None), + ("it('foo', function () {})", None), + ("it.concurrent('foo', function () {})", None), + ("fit('foo', function () {})", None), + ("fit.concurrent('foo', function () {})", None), + ("xit('foo', function () {})", None), + ("test('foo', function () {})", None), + ("test.concurrent('foo', function () {})", None), + ("xtest('foo', function () {})", None), + ("xtest(`foo`, function () {})", None), + ("someFn('foo', function () {})", None), + ( + " + describe('foo', () => { + it('bar', () => {}) + }) + ", + None, + ), + ( + "it(`GIVEN... + `, () => {});", + Some(serde_json::json!([{ "ignoreSpaces": true }])), + ), + ("describe('foo', function () {})", None), + ("fdescribe('foo', function () {})", None), + ("xdescribe('foo', function () {})", None), + ("xdescribe(`foo`, function () {})", None), + ("test('foo', function () {})", None), + ("test('foo', function () {})", None), + ("xtest('foo', function () {})", None), + ("xtest(`foo`, function () {})", None), + ("test('foo test', function () {})", None), + ("xtest('foo test', function () {})", None), + ("it('foo', function () {})", None), + ("fit('foo', function () {})", None), + ("xit('foo', function () {})", None), + ("xit(`foo`, function () {})", None), + ("it('foos it correctly', function () {})", None), + ( + " + describe('foo', () => { + it('bar', () => {}) + }) + ", + None, + ), + ( + " + describe('foo', () => { + it('describes things correctly', () => {}) + }) + ", + None, + ), + ]; + + let fail = vec![ + ( + "test('the correct way to properly handle all things', () => {});", + Some(serde_json::json!([{ "disallowedWords": ["correct", "properly", "all"] }])), + ), + ( + "describe('the correct way to do things', function () {})", + Some(serde_json::json!([{ "disallowedWords": ["correct"] }])), + ), + ( + "it('has ALL the things', () => {})", + Some(serde_json::json!([{ "disallowedWords": ["all"] }])), + ), + ( + "xdescribe('every single one of them', function () {})", + Some(serde_json::json!([{ "disallowedWords": ["every"] }])), + ), + ( + "describe('Very Descriptive Title Goes Here', function () {})", + Some(serde_json::json!([{ "disallowedWords": ["descriptive"] }])), + ), + ( + "test(`that the value is set properly`, function () {})", + Some(serde_json::json!([{ "disallowedWords": ["properly"] }])), + ), + // TODO: The regex `(?:#(?!unit|e2e))\w+` in those test cases is not valid in Rust + // ( + // " + // describe('things to test', () => { + // describe('unit tests #unit', () => { + // it('is true', () => { + // expect(true).toBe(true); + // }); + // }); + + // describe('e2e tests #e4e', () => { + // it('is another test #e2e #jest4life', () => {}); + // }); + // }); + // ", + // Some(serde_json::json!([ + // { + // "mustNotMatch": r#"(?:#(?!unit|e2e))\w+"#, + // "mustMatch": "^[^#]+$|(?:#(?:unit|e2e))", + // }, + // ])), + // ), + // ( + // " + // import { describe, describe as context, it as thisTest } from '@jest/globals'; + + // describe('things to test', () => { + // context('unit tests #unit', () => { + // thisTest('is true', () => { + // expect(true).toBe(true); + // }); + // }); + + // context('e2e tests #e4e', () => { + // thisTest('is another test #e2e #jest4life', () => {}); + // }); + // }); + // ", + // Some( + // serde_json::json!([ { "mustNotMatch": r#"(?:#(?!unit|e2e))\w+"#, "mustMatch": "^[^#]+$|(?:#(?:unit|e2e))", }, ]), + // ), + // ), + // ( + // " + // describe('things to test', () => { + // describe('unit tests #unit', () => { + // it('is true', () => { + // expect(true).toBe(true); + // }); + // }); + + // describe('e2e tests #e4e', () => { + // it('is another test #e2e #jest4life', () => {}); + // }); + // }); + // ", + // Some(serde_json::json!([ + // { + // "mustNotMatch": [ + // r#"(?:#(?!unit|e2e))\w+"#, + // "Please include '#unit' or '#e2e' in titles", + // ], + // "mustMatch": [ + // "^[^#]+$|(?:#(?:unit|e2e))", + // "Please include '#unit' or '#e2e' in titles", + // ], + // }, + // ])), + // ), + // ( + // " + // describe('things to test', () => { + // describe('unit tests #unit', () => { + // it('is true', () => { + // expect(true).toBe(true); + // }); + // }); + + // describe('e2e tests #e4e', () => { + // it('is another test #e2e #jest4life', () => {}); + // }); + // }); + // ", + // Some(serde_json::json!([ + // { + // "mustNotMatch": { "describe": [r#"(?:#(?!unit|e2e))\w+"#] }, + // "mustMatch": { "describe": "^[^#]+$|(?:#(?:unit|e2e))" }, + // }, + // ])), + // ), + // ( + // " + // describe('things to test', () => { + // describe('unit tests #unit', () => { + // it('is true', () => { + // expect(true).toBe(true); + // }); + // }); + + // describe('e2e tests #e4e', () => { + // it('is another test #e2e #jest4life', () => {}); + // }); + // }); + // ", + // Some(serde_json::json!([ + // { + // "mustNotMatch": { + // "describe": [ + // r#"(?:#(?!unit|e2e))\w+"#, + // "Please include '#unit' or '#e2e' in describe titles", + // ], + // }, + // "mustMatch": { "describe": "^[^#]+$|(?:#(?:unit|e2e))" }, + // }, + // ])), + // ), + // ( + // " + // describe('things to test', () => { + // describe('unit tests #unit', () => { + // it('is true', () => { + // expect(true).toBe(true); + // }); + // }); + + // describe('e2e tests #e4e', () => { + // it('is another test #e2e #jest4life', () => {}); + // }); + // }); + // ", + // Some(serde_json::json!([ + // { + // "mustNotMatch": { "describe": r#"(?:#(?!unit|e2e))\w+"# }, + // "mustMatch": { "it": "^[^#]+$|(?:#(?:unit|e2e))" }, + // }, + // ])), + // ), + // ( + // " + // describe('things to test', () => { + // describe('unit tests #unit', () => { + // it('is true #jest4life', () => { + // expect(true).toBe(true); + // }); + // }); + + // describe('e2e tests #e4e', () => { + // it('is another test #e2e #jest4life', () => {}); + // }); + // }); + // ", + // Some(serde_json::json!([ + // { + // "mustNotMatch": { + // "describe": [ + // r#"(?:#(?!unit|e2e))\w+"#, + // "Please include '#unit' or '#e2e' in describe titles", + // ], + // }, + // "mustMatch": { + // "it": [ + // "^[^#]+$|(?:#(?:unit|e2e))", + // "Please include '#unit' or '#e2e' in it titles", + // ], + // }, + // }, + // ])), + // ), + ( + "test('the correct way to properly handle all things', () => {});", + Some(serde_json::json!([{ "mustMatch": "#(?:unit|integration|e2e)" }])), + ), + ( + "describe('the test', () => {});", + Some(serde_json::json!([ + { "mustMatch": { "describe": "#(?:unit|integration|e2e)" } }, + ])), + ), + ( + "xdescribe('the test', () => {});", + Some(serde_json::json!([ + { "mustMatch": { "describe": "#(?:unit|integration|e2e)" } }, + ])), + ), + ( + "describe.skip('the test', () => {});", + Some(serde_json::json!([ + { "mustMatch": { "describe": "#(?:unit|integration|e2e)" } }, + ])), + ), + ("it.each([])(1, () => {});", None), + ("it.skip.each([])(1, () => {});", None), + ("it.skip.each``(1, () => {});", None), + ("it(123, () => {});", None), + ("it.concurrent(123, () => {});", None), + ("it(1 + 2 + 3, () => {});", None), + ("it.concurrent(1 + 2 + 3, () => {});", None), + ( + "test.skip(123, () => {});", + Some(serde_json::json!([{ "ignoreTypeOfDescribeName": true }])), + ), + ("describe(String(/.+/), () => {});", None), + ( + "describe(myFunction, () => 1);", + Some(serde_json::json!([{ "ignoreTypeOfDescribeName": false }])), + ), + ("describe(myFunction, () => {});", None), + ("xdescribe(myFunction, () => {});", None), + ("describe(6, function () {})", None), + ("describe.skip(123, () => {});", None), + ("describe('', function () {})", None), + ( + " + describe('foo', () => { + it('', () => {}); + }); + ", + None, + ), + ("it('', function () {})", None), + ("it.concurrent('', function () {})", None), + ("test('', function () {})", None), + ("test.concurrent('', function () {})", None), + ("test(``, function () {})", None), + ("test.concurrent(``, function () {})", None), + ("xdescribe('', () => {})", None), + ("xit('', () => {})", None), + ("xtest('', () => {})", None), + ("describe(' foo', function () {})", None), + ("describe.each()(' foo', function () {})", None), + ("describe.only.each()(' foo', function () {})", None), + ("describe(' foo foe fum', function () {})", None), + ("describe('foo foe fum ', function () {})", None), + ("fdescribe(' foo', function () {})", None), + ("fdescribe(' foo', function () {})", None), + ("xdescribe(' foo', function () {})", None), + ("it(' foo', function () {})", None), + ("it.concurrent(' foo', function () {})", None), + ("fit(' foo', function () {})", None), + ("it.skip(' foo', function () {})", None), + ("fit('foo ', function () {})", None), + ("it.skip('foo ', function () {})", None), + ( + " + import { test as testThat } from '@jest/globals'; + + testThat('foo works ', () => {}); + ", + None, + ), + ("xit(' foo', function () {})", None), + ("test(' foo', function () {})", None), + ("test.concurrent(' foo', function () {})", None), + ("test(` foo`, function () {})", None), + ("test.concurrent(` foo`, function () {})", None), + ("test(` foo bar bang`, function () {})", None), + ("test.concurrent(` foo bar bang`, function () {})", None), + ("test(` foo bar bang `, function () {})", None), + ("test.concurrent(` foo bar bang `, function () {})", None), + ("xtest(' foo', function () {})", None), + ("xtest(' foo ', function () {})", None), + ( + " + describe(' foo', () => { + it('bar', () => {}) + }) + ", + None, + ), + ( + " + describe('foo', () => { + it(' bar', () => {}) + }) + ", + None, + ), + ("describe('describe foo', function () {})", None), + ("fdescribe('describe foo', function () {})", None), + ("xdescribe('describe foo', function () {})", None), + ("describe('describe foo', function () {})", None), + ("fdescribe(`describe foo`, function () {})", None), + ("test('test foo', function () {})", None), + ("xtest('test foo', function () {})", None), + ("test(`test foo`, function () {})", None), + ("test(`test foo test`, function () {})", None), + ("it('it foo', function () {})", None), + ("fit('it foo', function () {})", None), + ("xit('it foo', function () {})", None), + ("it('it foos it correctly', function () {})", None), + ( + " + describe('describe foo', () => { + it('bar', () => {}) + }) + ", + None, + ), + ( + " + describe('describe foo', () => { + it('describes things correctly', () => {}) + }) + ", + None, + ), + ( + " + describe('foo', () => { + it('it bar', () => {}) + }) + ", + None, + ), + ]; + + Tester::new(ValidTitle::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/valid_title.snap b/crates/oxc_linter/src/snapshots/valid_title.snap new file mode 100644 index 000000000..d5473d20a --- /dev/null +++ b/crates/oxc_linter/src/snapshots/valid_title.snap @@ -0,0 +1,572 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: valid_title +--- + ⚠ eslint(jest/valid-title): "correct is not allowed in test title" + ╭─[valid_title.tsx:1:1] + 1 │ test('the correct way to properly handle all things', () => {}); + · ─────────────────────────────────────────────── + ╰──── + help: "It is included in the `disallowedWords` of your config file, try to remove it from your title" + + ⚠ eslint(jest/valid-title): "correct is not allowed in test title" + ╭─[valid_title.tsx:1:1] + 1 │ describe('the correct way to do things', function () {}) + · ────────────────────────────── + ╰──── + help: "It is included in the `disallowedWords` of your config file, try to remove it from your title" + + ⚠ eslint(jest/valid-title): "ALL is not allowed in test title" + ╭─[valid_title.tsx:1:1] + 1 │ it('has ALL the things', () => {}) + · ──────────────────── + ╰──── + help: "It is included in the `disallowedWords` of your config file, try to remove it from your title" + + ⚠ eslint(jest/valid-title): "every is not allowed in test title" + ╭─[valid_title.tsx:1:1] + 1 │ xdescribe('every single one of them', function () {}) + · ────────────────────────── + ╰──── + help: "It is included in the `disallowedWords` of your config file, try to remove it from your title" + + ⚠ eslint(jest/valid-title): "Descriptive is not allowed in test title" + ╭─[valid_title.tsx:1:1] + 1 │ describe('Very Descriptive Title Goes Here', function () {}) + · ────────────────────────────────── + ╰──── + help: "It is included in the `disallowedWords` of your config file, try to remove it from your title" + + ⚠ eslint(jest/valid-title): "properly is not allowed in test title" + ╭─[valid_title.tsx:1:1] + 1 │ test(`that the value is set properly`, function () {}) + · ──────────────────────────────── + ╰──── + help: "It is included in the `disallowedWords` of your config file, try to remove it from your title" + + ⚠ eslint(jest/valid-title): "test should match (?u)#(?:unit|integration|e2e)" + ╭─[valid_title.tsx:1:1] + 1 │ test('the correct way to properly handle all things', () => {}); + · ─────────────────────────────────────────────── + ╰──── + help: "Make sure the title matches the `mustMatch` of your config file" + + ⚠ eslint(jest/valid-title): "describe should match (?u)#(?:unit|integration|e2e)" + ╭─[valid_title.tsx:1:1] + 1 │ describe('the test', () => {}); + · ────────── + ╰──── + help: "Make sure the title matches the `mustMatch` of your config file" + + ⚠ eslint(jest/valid-title): "describe should match (?u)#(?:unit|integration|e2e)" + ╭─[valid_title.tsx:1:1] + 1 │ xdescribe('the test', () => {}); + · ────────── + ╰──── + help: "Make sure the title matches the `mustMatch` of your config file" + + ⚠ eslint(jest/valid-title): "describe should match (?u)#(?:unit|integration|e2e)" + ╭─[valid_title.tsx:1:1] + 1 │ describe.skip('the test', () => {}); + · ────────── + ╰──── + help: "Make sure the title matches the `mustMatch` of your config file" + + ⚠ eslint(jest/valid-title): "Title must be a string" + ╭─[valid_title.tsx:1:1] + 1 │ it.each([])(1, () => {}); + · ─ + ╰──── + help: "Replace your title with a string" + + ⚠ eslint(jest/valid-title): "Title must be a string" + ╭─[valid_title.tsx:1:1] + 1 │ it.skip.each([])(1, () => {}); + · ─ + ╰──── + help: "Replace your title with a string" + + ⚠ eslint(jest/valid-title): "Title must be a string" + ╭─[valid_title.tsx:1:1] + 1 │ it.skip.each``(1, () => {}); + · ─ + ╰──── + help: "Replace your title with a string" + + ⚠ eslint(jest/valid-title): "Title must be a string" + ╭─[valid_title.tsx:1:1] + 1 │ it(123, () => {}); + · ─── + ╰──── + help: "Replace your title with a string" + + ⚠ eslint(jest/valid-title): "Title must be a string" + ╭─[valid_title.tsx:1:1] + 1 │ it.concurrent(123, () => {}); + · ─── + ╰──── + help: "Replace your title with a string" + + ⚠ eslint(jest/valid-title): "Title must be a string" + ╭─[valid_title.tsx:1:1] + 1 │ it(1 + 2 + 3, () => {}); + · ───────── + ╰──── + help: "Replace your title with a string" + + ⚠ eslint(jest/valid-title): "Title must be a string" + ╭─[valid_title.tsx:1:1] + 1 │ it.concurrent(1 + 2 + 3, () => {}); + · ───────── + ╰──── + help: "Replace your title with a string" + + ⚠ eslint(jest/valid-title): "Title must be a string" + ╭─[valid_title.tsx:1:1] + 1 │ test.skip(123, () => {}); + · ─── + ╰──── + help: "Replace your title with a string" + + ⚠ eslint(jest/valid-title): "Title must be a string" + ╭─[valid_title.tsx:1:1] + 1 │ describe(String(/.+/), () => {}); + · ──────────── + ╰──── + help: "Replace your title with a string" + + ⚠ eslint(jest/valid-title): "Title must be a string" + ╭─[valid_title.tsx:1:1] + 1 │ describe(myFunction, () => 1); + · ────────── + ╰──── + help: "Replace your title with a string" + + ⚠ eslint(jest/valid-title): "Title must be a string" + ╭─[valid_title.tsx:1:1] + 1 │ describe(myFunction, () => {}); + · ────────── + ╰──── + help: "Replace your title with a string" + + ⚠ eslint(jest/valid-title): "Title must be a string" + ╭─[valid_title.tsx:1:1] + 1 │ xdescribe(myFunction, () => {}); + · ────────── + ╰──── + help: "Replace your title with a string" + + ⚠ eslint(jest/valid-title): "Title must be a string" + ╭─[valid_title.tsx:1:1] + 1 │ describe(6, function () {}) + · ─ + ╰──── + help: "Replace your title with a string" + + ⚠ eslint(jest/valid-title): "Title must be a string" + ╭─[valid_title.tsx:1:1] + 1 │ describe.skip(123, () => {}); + · ─── + ╰──── + help: "Replace your title with a string" + + ⚠ eslint(jest/valid-title): "Should not have an empty title" + ╭─[valid_title.tsx:1:1] + 1 │ describe('', function () {}) + · ── + ╰──── + help: "Write a meaningful title for your test" + + ⚠ eslint(jest/valid-title): "Should not have an empty title" + ╭─[valid_title.tsx:2:1] + 2 │ describe('foo', () => { + 3 │ it('', () => {}); + · ── + 4 │ }); + ╰──── + help: "Write a meaningful title for your test" + + ⚠ eslint(jest/valid-title): "Should not have an empty title" + ╭─[valid_title.tsx:1:1] + 1 │ it('', function () {}) + · ── + ╰──── + help: "Write a meaningful title for your test" + + ⚠ eslint(jest/valid-title): "Should not have an empty title" + ╭─[valid_title.tsx:1:1] + 1 │ it.concurrent('', function () {}) + · ── + ╰──── + help: "Write a meaningful title for your test" + + ⚠ eslint(jest/valid-title): "Should not have an empty title" + ╭─[valid_title.tsx:1:1] + 1 │ test('', function () {}) + · ── + ╰──── + help: "Write a meaningful title for your test" + + ⚠ eslint(jest/valid-title): "Should not have an empty title" + ╭─[valid_title.tsx:1:1] + 1 │ test.concurrent('', function () {}) + · ── + ╰──── + help: "Write a meaningful title for your test" + + ⚠ eslint(jest/valid-title): "Should not have an empty title" + ╭─[valid_title.tsx:1:1] + 1 │ test(``, function () {}) + · ── + ╰──── + help: "Write a meaningful title for your test" + + ⚠ eslint(jest/valid-title): "Should not have an empty title" + ╭─[valid_title.tsx:1:1] + 1 │ test.concurrent(``, function () {}) + · ── + ╰──── + help: "Write a meaningful title for your test" + + ⚠ eslint(jest/valid-title): "Should not have an empty title" + ╭─[valid_title.tsx:1:1] + 1 │ xdescribe('', () => {}) + · ── + ╰──── + help: "Write a meaningful title for your test" + + ⚠ eslint(jest/valid-title): "Should not have an empty title" + ╭─[valid_title.tsx:1:1] + 1 │ xit('', () => {}) + · ── + ╰──── + help: "Write a meaningful title for your test" + + ⚠ eslint(jest/valid-title): "Should not have an empty title" + ╭─[valid_title.tsx:1:1] + 1 │ xtest('', () => {}) + · ── + ╰──── + help: "Write a meaningful title for your test" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ describe(' foo', function () {}) + · ────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ describe.each()(' foo', function () {}) + · ────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ describe.only.each()(' foo', function () {}) + · ────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ describe(' foo foe fum', function () {}) + · ────────────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ describe('foo foe fum ', function () {}) + · ────────────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ fdescribe(' foo', function () {}) + · ────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ fdescribe(' foo', function () {}) + · ────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ xdescribe(' foo', function () {}) + · ────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ it(' foo', function () {}) + · ────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ it.concurrent(' foo', function () {}) + · ────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ fit(' foo', function () {}) + · ────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ it.skip(' foo', function () {}) + · ────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ fit('foo ', function () {}) + · ────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ it.skip('foo ', function () {}) + · ────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:3:1] + 3 │ + 4 │ testThat('foo works ', () => {}); + · ──────────── + 5 │ + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ xit(' foo', function () {}) + · ────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ test(' foo', function () {}) + · ────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ test.concurrent(' foo', function () {}) + · ────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ test(` foo`, function () {}) + · ────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ test.concurrent(` foo`, function () {}) + · ────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ test(` foo bar bang`, function () {}) + · ─────────────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ test.concurrent(` foo bar bang`, function () {}) + · ─────────────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ test(` foo bar bang `, function () {}) + · ───────────────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ test.concurrent(` foo bar bang `, function () {}) + · ───────────────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ xtest(' foo', function () {}) + · ────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ xtest(' foo ', function () {}) + · ──────── + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:1:1] + 1 │ + 2 │ describe(' foo', () => { + · ────── + 3 │ it('bar', () => {}) + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces" + ╭─[valid_title.tsx:2:1] + 2 │ describe('foo', () => { + 3 │ it(' bar', () => {}) + · ────── + 4 │ }) + ╰──── + help: "Remove the leading or trailing spaces" + + ⚠ eslint(jest/valid-title): "Should not have duplicate prefix" + ╭─[valid_title.tsx:1:1] + 1 │ describe('describe foo', function () {}) + · ────────────── + ╰──── + help: "The function name has already contains the prefix, try remove the duplicate prefix" + + ⚠ eslint(jest/valid-title): "Should not have duplicate prefix" + ╭─[valid_title.tsx:1:1] + 1 │ fdescribe('describe foo', function () {}) + · ────────────── + ╰──── + help: "The function name has already contains the prefix, try remove the duplicate prefix" + + ⚠ eslint(jest/valid-title): "Should not have duplicate prefix" + ╭─[valid_title.tsx:1:1] + 1 │ xdescribe('describe foo', function () {}) + · ────────────── + ╰──── + help: "The function name has already contains the prefix, try remove the duplicate prefix" + + ⚠ eslint(jest/valid-title): "Should not have duplicate prefix" + ╭─[valid_title.tsx:1:1] + 1 │ describe('describe foo', function () {}) + · ────────────── + ╰──── + help: "The function name has already contains the prefix, try remove the duplicate prefix" + + ⚠ eslint(jest/valid-title): "Should not have duplicate prefix" + ╭─[valid_title.tsx:1:1] + 1 │ fdescribe(`describe foo`, function () {}) + · ────────────── + ╰──── + help: "The function name has already contains the prefix, try remove the duplicate prefix" + + ⚠ eslint(jest/valid-title): "Should not have duplicate prefix" + ╭─[valid_title.tsx:1:1] + 1 │ test('test foo', function () {}) + · ────────── + ╰──── + help: "The function name has already contains the prefix, try remove the duplicate prefix" + + ⚠ eslint(jest/valid-title): "Should not have duplicate prefix" + ╭─[valid_title.tsx:1:1] + 1 │ xtest('test foo', function () {}) + · ────────── + ╰──── + help: "The function name has already contains the prefix, try remove the duplicate prefix" + + ⚠ eslint(jest/valid-title): "Should not have duplicate prefix" + ╭─[valid_title.tsx:1:1] + 1 │ test(`test foo`, function () {}) + · ────────── + ╰──── + help: "The function name has already contains the prefix, try remove the duplicate prefix" + + ⚠ eslint(jest/valid-title): "Should not have duplicate prefix" + ╭─[valid_title.tsx:1:1] + 1 │ test(`test foo test`, function () {}) + · ─────────────── + ╰──── + help: "The function name has already contains the prefix, try remove the duplicate prefix" + + ⚠ eslint(jest/valid-title): "Should not have duplicate prefix" + ╭─[valid_title.tsx:1:1] + 1 │ it('it foo', function () {}) + · ──────── + ╰──── + help: "The function name has already contains the prefix, try remove the duplicate prefix" + + ⚠ eslint(jest/valid-title): "Should not have duplicate prefix" + ╭─[valid_title.tsx:1:1] + 1 │ fit('it foo', function () {}) + · ──────── + ╰──── + help: "The function name has already contains the prefix, try remove the duplicate prefix" + + ⚠ eslint(jest/valid-title): "Should not have duplicate prefix" + ╭─[valid_title.tsx:1:1] + 1 │ xit('it foo', function () {}) + · ──────── + ╰──── + help: "The function name has already contains the prefix, try remove the duplicate prefix" + + ⚠ eslint(jest/valid-title): "Should not have duplicate prefix" + ╭─[valid_title.tsx:1:1] + 1 │ it('it foos it correctly', function () {}) + · ────────────────────── + ╰──── + help: "The function name has already contains the prefix, try remove the duplicate prefix" + + ⚠ eslint(jest/valid-title): "Should not have duplicate prefix" + ╭─[valid_title.tsx:1:1] + 1 │ + 2 │ describe('describe foo', () => { + · ────────────── + 3 │ it('bar', () => {}) + ╰──── + help: "The function name has already contains the prefix, try remove the duplicate prefix" + + ⚠ eslint(jest/valid-title): "Should not have duplicate prefix" + ╭─[valid_title.tsx:1:1] + 1 │ + 2 │ describe('describe foo', () => { + · ────────────── + 3 │ it('describes things correctly', () => {}) + ╰──── + help: "The function name has already contains the prefix, try remove the duplicate prefix" + + ⚠ eslint(jest/valid-title): "Should not have duplicate prefix" + ╭─[valid_title.tsx:2:1] + 2 │ describe('foo', () => { + 3 │ it('it bar', () => {}) + · ──────── + 4 │ }) + ╰──── + help: "The function name has already contains the prefix, try remove the duplicate prefix" + +