From 5d9d15da2442e2752668e6b4338d228e0d94c93b Mon Sep 17 00:00:00 2001 From: wenzhe Date: Sat, 7 Oct 2023 22:42:22 +0800 Subject: [PATCH 1/5] wip: save --- crates/oxc_linter/src/rules.rs | 2 + .../oxc_linter/src/rules/jest/valid_title.rs | 858 ++++++++++++++++++ 2 files changed, 860 insertions(+) create mode 100644 crates/oxc_linter/src/rules/jest/valid_title.rs diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index c4a8fdffba374..e6dfe2d823ad4 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 0000000000000..82bc8db8825f1 --- /dev/null +++ b/crates/oxc_linter/src/rules/jest/valid_title.rs @@ -0,0 +1,858 @@ +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::{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("")] +#[diagnostic(severity(warning), help(""))] +struct ValidTitleDiagnostic(&'static str, &'static str, #[label] pub Span); + +#[derive(Debug, 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, +} + +type CompiledMatcherAndMessage = (Regex, Option); + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +enum MatchKind { + Describe, + It, + Test, +} + +impl MatchKind { + fn from(name: &str) -> Option { + match name { + "describe" => Some(Self::Describe), + "it" => Some(Self::It), + "test" => Some(Self::Test), + _ => None, + } + } +} + +impl Default for ValidTitle { + fn default() -> Self { + Self { + ignore_type_of_describe_name: false, + disallowed_words: vec![], + ignore_space: false, + must_not_match_patterns: HashMap::new(), + must_match_patterns: HashMap::new(), + } + } +} + +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 +); + +fn compile_matcher_patterns( + matcher_patterns: &serde_json::Value, +) -> Option> { + matcher_patterns + .as_array() + .map_or_else( + || { + // for `{ "describe": "/pattern/u" }` + 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); + } + } + + return Some(map); + }, + |value| { + // for `["/pattern/u", "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/u"` + 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) + }, + |map| Some(map), + ) +} + +enum MatcherPattern<'a> { + String(&'a serde_json::Value), + Vec(&'a Vec), +} + +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(|v| v.to_string()); + Some((reg, message)) + } + } +} + +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(|v| v.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(|v| v.to_string())).collect()) + .unwrap_or_default(); + let must_not_match_patterns = config + .and_then(|v| v.get("mustNotMatch")) + .and_then(|v| compile_matcher_patterns(v)) + .unwrap_or_default(); + let must_match_patterns = config + .and_then(|v| v.get("mustMatch")) + .and_then(|v| compile_matcher_patterns(v)) + .unwrap_or_default(); + + ValidTitle { + ignore_type_of_describe_name, + ignore_space, + disallowed_words, + must_match_patterns, + must_not_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 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()); + } + } + } + } +} + +fn validate_title(title: &str, span: Span, valid_title: &ValidTitle, name: &str, ctx: &LintContext) { + if title == "" { + 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 disallowed_words_reg.is_match(title) { + Message::DisallowedWord.diagnostic(ctx, span); + } + return; + } + + 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; + }; + + 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) { + Message::MustMatch.diagnostic(ctx, span); + } + return; + } + + if let Some((regex, _)) = valid_title.must_not_match_patterns.get(&jest_fn_name) { + if regex.is_match(title) { + Message::MustNotMatch.diagnostic(ctx, span); + } + return; + } +} + +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, + DisallowedWord, + MustNotMatch, + MustMatch, + MustNotMatchCustom, + MustMatchCustom, +} + +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 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"), + Self::DisallowedWord => ("is not allowed in test titles", ""), + Self::MustNotMatch => (" should not match", ""), + Self::MustMatch => ("jestFunctionName should match pattern", ""), + Self::MustNotMatchCustom => ("message ", "me-"), + Self::MustMatchCustom => ("message", "mess"), + } + } + fn diagnostic(&self, ctx: &LintContext, span: Span) { + let (error, help) = self.detail(); + ctx.diagnostic(ValidTitleDiagnostic(error, help, span)); + } +} + +#[ignore] +#[test] +fn test_1() { + use crate::tester::Tester; + let pass = vec![("test('foo test', function () {})", None)]; + let fail = vec![]; + + Tester::new(ValidTitle::NAME, pass, fail).test_and_snapshot(); +} + +#[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"] }])), + ), + ( + " + 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(); +} From a5935eb618cb13c731ca96ccde44240934c98a9f Mon Sep 17 00:00:00 2001 From: wenzhe Date: Sun, 8 Oct 2023 20:46:35 +0800 Subject: [PATCH 2/5] feat(linter): add `jest/valid-title` rule --- .../oxc_linter/src/rules/jest/valid_title.rs | 469 +++++++------- .../oxc_linter/src/snapshots/valid_title.snap | 572 ++++++++++++++++++ 2 files changed, 810 insertions(+), 231 deletions(-) create mode 100644 crates/oxc_linter/src/snapshots/valid_title.snap diff --git a/crates/oxc_linter/src/rules/jest/valid_title.rs b/crates/oxc_linter/src/rules/jest/valid_title.rs index 82bc8db8825f1..5cd595829fe21 100644 --- a/crates/oxc_linter/src/rules/jest/valid_title.rs +++ b/crates/oxc_linter/src/rules/jest/valid_title.rs @@ -9,7 +9,7 @@ use oxc_diagnostics::{ thiserror::Error, }; use oxc_macros::declare_oxc_lint; -use oxc_span::{GetSpan, Span}; +use oxc_span::{Atom, GetSpan, Span}; use regex::Regex; use crate::{ @@ -20,11 +20,11 @@ use crate::{ }; #[derive(Debug, Error, Diagnostic)] -#[error("")] -#[diagnostic(severity(warning), help(""))] -struct ValidTitleDiagnostic(&'static str, &'static str, #[label] pub Span); +#[error("eslint(jest/valid-title): {0:?}")] +#[diagnostic(severity(warning), help("{1:?}"))] +struct ValidTitleDiagnostic(Atom, &'static str, #[label] pub Span); -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone)] pub struct ValidTitle { ignore_type_of_describe_name: bool, disallowed_words: Vec, @@ -53,18 +53,6 @@ impl MatchKind { } } -impl Default for ValidTitle { - fn default() -> Self { - Self { - ignore_type_of_describe_name: false, - disallowed_words: vec![], - ignore_space: false, - must_not_match_patterns: HashMap::new(), - must_match_patterns: HashMap::new(), - } - } -} - declare_oxc_lint!( /// ### What it does /// @@ -110,12 +98,12 @@ fn compile_matcher_patterns( } } - return Some(map); + Some(map) }, |value| { // for `["/pattern/u", "message"]` let mut map: HashMap = HashMap::new(); - let v = compile_matcher_pattern(MatcherPattern::Vec(value))?; + 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()); @@ -127,7 +115,7 @@ fn compile_matcher_patterns( // for `"/pattern/u"` let string = matcher_patterns.as_str()?; let mut map: HashMap = HashMap::new(); - let v = compile_matcher_pattern(MatcherPattern::String( + let v = &compile_matcher_pattern(MatcherPattern::String( &serde_json::Value::String(string.to_string()), ))?; map.insert(MatchKind::Describe, v.clone()); @@ -135,10 +123,11 @@ fn compile_matcher_patterns( map.insert(MatchKind::It, v.clone()); Some(map) }, - |map| Some(map), + Some, ) } +#[derive(Copy, Clone)] enum MatcherPattern<'a> { String(&'a serde_json::Value), Vec(&'a Vec), @@ -154,7 +143,7 @@ fn compile_matcher_pattern(pattern: MatcherPattern) -> Option { 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(|v| v.to_string()); + let message = pattern.get(1).map(std::string::ToString::to_string); Some((reg, message)) } } @@ -164,7 +153,10 @@ 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(|v| v.as_bool()).unwrap_or_default() + 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"); @@ -172,23 +164,24 @@ impl Rule for ValidTitle { 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(|v| v.to_string())).collect()) + .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(|v| compile_matcher_patterns(v)) + .and_then(compile_matcher_patterns) .unwrap_or_default(); let must_match_patterns = config .and_then(|v| v.get("mustMatch")) - .and_then(|v| compile_matcher_patterns(v)) + .and_then(compile_matcher_patterns) .unwrap_or_default(); - - ValidTitle { + Self { ignore_type_of_describe_name, - ignore_space, disallowed_words, - must_match_patterns, + ignore_space, must_not_match_patterns, + must_match_patterns, } } fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { @@ -224,9 +217,12 @@ impl Rule for ValidTitle { ); } Expression::TemplateLiteral(template_literal) => { + if !template_literal.is_no_substitution_template() { + return; + } if let Some(quasi) = template_literal.quasi() { validate_title( - &quasi.as_str(), + quasi.as_str(), template_literal.span, self, &jest_fn_call.name, @@ -235,7 +231,7 @@ impl Rule for ValidTitle { } } Expression::BinaryExpression(binary_expr) => { - if does_binary_expression_contain_string_node(&binary_expr) { + if does_binary_expression_contain_string_node(binary_expr) { return; } if need_report_describe_name { @@ -251,25 +247,37 @@ impl Rule for ValidTitle { } } -fn validate_title(title: &str, span: Span, valid_title: &ValidTitle, name: &str, ctx: &LintContext) { - if title == "" { +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"\.") + valid_title.disallowed_words.join("|").replace('.', r"\.") )) else { return; }; - if disallowed_words_reg.is_match(title) { - Message::DisallowedWord.diagnostic(ctx, span); + 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); } @@ -279,6 +287,7 @@ fn validate_title(title: &str, span: Span, valid_title: &ValidTitle, name: &str, return; }; + // TODO: support fixer if first_word == un_prefixed_name { Message::DuplicatePrefix.diagnostic(ctx, span); return; @@ -290,16 +299,33 @@ fn validate_title(title: &str, span: Span, valid_title: &ValidTitle, name: &str, if let Some((regex, message)) = valid_title.must_match_patterns.get(&jest_fn_name) { if !regex.is_match(title) { - Message::MustMatch.diagnostic(ctx, span); + 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, + )); } - return; } - if let Some((regex, _)) = valid_title.must_not_match_patterns.get(&jest_fn_name) { + if let Some((regex, message)) = valid_title.must_not_match_patterns.get(&jest_fn_name) { if regex.is_match(title) { - Message::MustNotMatch.diagnostic(ctx, span); + 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, + )); } - return; } } @@ -319,43 +345,23 @@ enum Message { EmptyTitle, DuplicatePrefix, AccidentalSpace, - DisallowedWord, - MustNotMatch, - MustMatch, - MustNotMatchCustom, - MustMatchCustom, } 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 title for your test"), + 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"), - Self::DisallowedWord => ("is not allowed in test titles", ""), - Self::MustNotMatch => (" should not match", ""), - Self::MustMatch => ("jestFunctionName should match pattern", ""), - Self::MustNotMatchCustom => ("message ", "me-"), - Self::MustMatchCustom => ("message", "mess"), } } fn diagnostic(&self, ctx: &LintContext, span: Span) { let (error, help) = self.detail(); - ctx.diagnostic(ValidTitleDiagnostic(error, help, span)); + ctx.diagnostic(ValidTitleDiagnostic(Atom::from(error), help, span)); } } -#[ignore] -#[test] -fn test_1() { - use crate::tester::Tester; - let pass = vec![("test('foo test', function () {})", None)]; - let fail = vec![]; - - Tester::new(ValidTitle::NAME, pass, fail).test_and_snapshot(); -} - #[allow(clippy::too_many_lines)] #[test] fn test() { @@ -539,173 +545,174 @@ fn test() { "test(`that the value is set properly`, function () {})", Some(serde_json::json!([{ "disallowedWords": ["properly"] }])), ), - ( - " - 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", - ], - }, - }, - ])), - ), + // 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)" }])), 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 0000000000000..d5473d20ab6a1 --- /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" + + From cbb15b4b183193daea0c5515a7600fa9492ef8e7 Mon Sep 17 00:00:00 2001 From: wenzhe Date: Sun, 8 Oct 2023 20:50:55 +0800 Subject: [PATCH 3/5] refactor: move --- .../oxc_linter/src/rules/jest/valid_title.rs | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/crates/oxc_linter/src/rules/jest/valid_title.rs b/crates/oxc_linter/src/rules/jest/valid_title.rs index 5cd595829fe21..47b7141cfbe78 100644 --- a/crates/oxc_linter/src/rules/jest/valid_title.rs +++ b/crates/oxc_linter/src/rules/jest/valid_title.rs @@ -33,26 +33,6 @@ pub struct ValidTitle { must_match_patterns: HashMap, } -type CompiledMatcherAndMessage = (Regex, Option); - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -enum MatchKind { - Describe, - It, - Test, -} - -impl MatchKind { - fn from(name: &str) -> Option { - match name { - "describe" => Some(Self::Describe), - "it" => Some(Self::It), - "test" => Some(Self::Test), - _ => None, - } - } -} - declare_oxc_lint!( /// ### What it does /// @@ -127,6 +107,26 @@ fn compile_matcher_patterns( ) } +type CompiledMatcherAndMessage = (Regex, Option); + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +enum MatchKind { + Describe, + It, + Test, +} + +impl MatchKind { + fn from(name: &str) -> Option { + match name { + "describe" => Some(Self::Describe), + "it" => Some(Self::It), + "test" => Some(Self::Test), + _ => None, + } + } +} + #[derive(Copy, Clone)] enum MatcherPattern<'a> { String(&'a serde_json::Value), From 46e815b62078802a004dbe5608229371b8d23972 Mon Sep 17 00:00:00 2001 From: wenzhe Date: Sun, 8 Oct 2023 21:33:57 +0800 Subject: [PATCH 4/5] docs(linter): update --- crates/oxc_linter/src/rules/jest/valid_title.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/oxc_linter/src/rules/jest/valid_title.rs b/crates/oxc_linter/src/rules/jest/valid_title.rs index 47b7141cfbe78..dc595db2a2c51 100644 --- a/crates/oxc_linter/src/rules/jest/valid_title.rs +++ b/crates/oxc_linter/src/rules/jest/valid_title.rs @@ -66,7 +66,7 @@ fn compile_matcher_patterns( .as_array() .map_or_else( || { - // for `{ "describe": "/pattern/u" }` + // for `{ "describe": "/pattern/" }` let obj = matcher_patterns.as_object()?; let mut map: HashMap = HashMap::new(); for (key, value) in obj { @@ -81,7 +81,7 @@ fn compile_matcher_patterns( Some(map) }, |value| { - // for `["/pattern/u", "message"]` + // for `["/pattern/", "message"]` let mut map: HashMap = HashMap::new(); let v = &compile_matcher_pattern(MatcherPattern::Vec(value))?; map.insert(MatchKind::Describe, v.clone()); @@ -92,7 +92,7 @@ fn compile_matcher_patterns( ) .map_or_else( || { - // for `"/pattern/u"` + // for `"/pattern/"` let string = matcher_patterns.as_str()?; let mut map: HashMap = HashMap::new(); let v = &compile_matcher_pattern(MatcherPattern::String( From 475437005143ebf1f9f0775a38ecc73660b73f18 Mon Sep 17 00:00:00 2001 From: wenzhe Date: Sun, 8 Oct 2023 22:23:00 +0800 Subject: [PATCH 5/5] refactor: move function position for better read --- .../oxc_linter/src/rules/jest/valid_title.rs | 180 +++++++++--------- 1 file changed, 90 insertions(+), 90 deletions(-) diff --git a/crates/oxc_linter/src/rules/jest/valid_title.rs b/crates/oxc_linter/src/rules/jest/valid_title.rs index dc595db2a2c51..b6d374f83d653 100644 --- a/crates/oxc_linter/src/rules/jest/valid_title.rs +++ b/crates/oxc_linter/src/rules/jest/valid_title.rs @@ -59,96 +59,6 @@ declare_oxc_lint!( restriction ); -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, - ) -} - -type CompiledMatcherAndMessage = (Regex, Option); - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -enum MatchKind { - Describe, - It, - Test, -} - -impl MatchKind { - fn from(name: &str) -> Option { - match name { - "describe" => Some(Self::Describe), - "it" => Some(Self::It), - "test" => Some(Self::Test), - _ => None, - } - } -} - -#[derive(Copy, Clone)] -enum MatcherPattern<'a> { - String(&'a serde_json::Value), - Vec(&'a Vec), -} - -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)) - } - } -} - impl Rule for ValidTitle { fn from_configuration(value: serde_json::Value) -> Self { let config = value.get(0); @@ -247,6 +157,96 @@ impl Rule for ValidTitle { } } +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,