From c3fee4ee7c7279812a176577f389ac7df45f81bb Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 23 Jan 2024 12:15:38 +0000 Subject: [PATCH] feat(linter): add rule `noSkippedTests` (#1635) --- CHANGELOG.md | 19 + Cargo.lock | 1 + .../src/categories.rs | 2 + .../biome_js_analyze/src/analyzers/nursery.rs | 4 + .../src/analyzers/nursery/no_focused_tests.rs | 82 +++ .../src/analyzers/nursery/no_skipped_tests.rs | 91 +++ crates/biome_js_analyze/src/lib.rs | 14 +- .../specs/nursery/noFocusedTests/invalid.js | 5 + .../nursery/noFocusedTests/invalid.js.snap | 70 ++ .../specs/nursery/noFocusedTests/valid.js | 5 + .../nursery/noFocusedTests/valid.js.snap | 15 + .../specs/nursery/noSkippedTests/invalid.js | 6 + .../nursery/noSkippedTests/invalid.js.snap | 127 ++++ .../specs/nursery/noSkippedTests/valid.js | 7 + .../nursery/noSkippedTests/valid.js.snap | 17 + .../src/js/bindings/parameters.rs | 5 +- .../expressions/arrow_function_expression.rs | 7 +- .../src/js/expressions/call_arguments.rs | 3 +- .../src/js/expressions/template_expression.rs | 3 +- .../src/utils/member_chain/mod.rs | 3 +- crates/biome_js_formatter/src/utils/mod.rs | 1 - .../biome_js_formatter/src/utils/test_call.rs | 697 ------------------ crates/biome_js_syntax/Cargo.toml | 1 + crates/biome_js_syntax/src/expr_ext.rs | 684 ++++++++++++++++- crates/biome_js_syntax/src/lib.rs | 13 +- .../src/configuration/linter/rules.rs | 138 ++-- .../invalid/hooks_missing_name.json.snap | 2 + .../@biomejs/backend-jsonrpc/src/workspace.ts | 10 + .../@biomejs/biome/configuration_schema.json | 14 + .../components/generated/NumberOfRules.astro | 2 +- .../src/content/docs/internals/changelog.mdx | 19 + .../src/content/docs/linter/rules/index.mdx | 2 + .../docs/linter/rules/no-focused-tests.md | 66 ++ .../docs/linter/rules/no-skipped-tests.md | 71 ++ xtask/codegen/src/generate_new_lintrule.rs | 2 - 35 files changed, 1421 insertions(+), 787 deletions(-) create mode 100644 crates/biome_js_analyze/src/analyzers/nursery/no_focused_tests.rs create mode 100644 crates/biome_js_analyze/src/analyzers/nursery/no_skipped_tests.rs create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noFocusedTests/invalid.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noFocusedTests/invalid.js.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noFocusedTests/valid.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noFocusedTests/valid.js.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noSkippedTests/invalid.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noSkippedTests/invalid.js.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noSkippedTests/valid.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noSkippedTests/valid.js.snap delete mode 100644 crates/biome_js_formatter/src/utils/test_call.rs create mode 100644 website/src/content/docs/linter/rules/no-focused-tests.md create mode 100644 website/src/content/docs/linter/rules/no-skipped-tests.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b79a1faa1e0..b6abc676419c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,25 @@ Read our [guidelines for writing a good changelog entry](https://github.com/biom ### Linter +#### New features + +- Add the rule [noSkippedTests](https://biomejs.dev/linter/rules/no-skipped-tests), to disallow skipped tests: + + ```js + describe.skip("test", () => {}); + it.skip("test", () => {}); + ``` +<<<<<<< HEAD +======= + +- Add the rule [noFocusedTests](https://biomejs.dev/linter/rules/no-focused-tests), to disallow skipped tests: + + ```js + describe.only("test", () => {}); + it.only("test", () => {}); + ``` + +>>>>>>> fd3de977d1 (feat(linter): new rule noFocusedTests (#1641)) ### Parser ## 1.5.3 (2024-01-22) diff --git a/Cargo.lock b/Cargo.lock index 30443bc26a52..d22bd78dd97a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -575,6 +575,7 @@ dependencies = [ "biome_console", "biome_diagnostics", "biome_js_factory", + "biome_js_parser", "biome_rowan", "schemars", "serde", diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index e1e22e230fe5..aed9bb70b566 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -105,11 +105,13 @@ define_categories! { "lint/nursery/noDuplicateJsonKeys": "https://biomejs.dev/linter/rules/no-duplicate-json-keys", "lint/nursery/noEmptyBlockStatements": "https://biomejs.dev/linter/rules/no-empty-block-statements", "lint/nursery/noEmptyTypeParameters": "https://biomejs.dev/linter/rules/no-empty-type-parameters", + "lint/nursery/noFocusedTests": "https://biomejs.dev/linter/rules/no-focused-tests", "lint/nursery/noGlobalAssign": "https://biomejs.dev/linter/rules/no-global-assign", "lint/nursery/noGlobalEval": "https://biomejs.dev/linter/rules/no-global-eval", "lint/nursery/noInvalidUseBeforeDeclaration": "https://biomejs.dev/linter/rules/no-invalid-use-before-declaration", "lint/nursery/noMisleadingCharacterClass": "https://biomejs.dev/linter/rules/no-misleading-character-class", "lint/nursery/noNodejsModules": "https://biomejs.dev/linter/rules/no-nodejs-modules", + "lint/nursery/noSkippedTests": "https://biomejs.dev/linter/rules/no-skipped-tests", "lint/nursery/noThenProperty": "https://biomejs.dev/linter/rules/no-then-property", "lint/nursery/noTypeOnlyImportAttributes": "https://biomejs.dev/linter/rules/no-type-only-import-attributes", "lint/nursery/noUnusedImports": "https://biomejs.dev/linter/rules/no-unused-imports", diff --git a/crates/biome_js_analyze/src/analyzers/nursery.rs b/crates/biome_js_analyze/src/analyzers/nursery.rs index ed5af4dfecb0..ccff9dbbfe84 100644 --- a/crates/biome_js_analyze/src/analyzers/nursery.rs +++ b/crates/biome_js_analyze/src/analyzers/nursery.rs @@ -4,7 +4,9 @@ use biome_analyze::declare_group; pub(crate) mod no_empty_block_statements; pub(crate) mod no_empty_type_parameters; +pub(crate) mod no_focused_tests; pub(crate) mod no_nodejs_modules; +pub(crate) mod no_skipped_tests; pub(crate) mod no_unused_private_class_members; pub(crate) mod no_useless_lone_block_statements; pub(crate) mod no_useless_ternary; @@ -22,7 +24,9 @@ declare_group! { rules : [ self :: no_empty_block_statements :: NoEmptyBlockStatements , self :: no_empty_type_parameters :: NoEmptyTypeParameters , + self :: no_focused_tests :: NoFocusedTests , self :: no_nodejs_modules :: NoNodejsModules , + self :: no_skipped_tests :: NoSkippedTests , self :: no_unused_private_class_members :: NoUnusedPrivateClassMembers , self :: no_useless_lone_block_statements :: NoUselessLoneBlockStatements , self :: no_useless_ternary :: NoUselessTernary , diff --git a/crates/biome_js_analyze/src/analyzers/nursery/no_focused_tests.rs b/crates/biome_js_analyze/src/analyzers/nursery/no_focused_tests.rs new file mode 100644 index 000000000000..7a8afc016873 --- /dev/null +++ b/crates/biome_js_analyze/src/analyzers/nursery/no_focused_tests.rs @@ -0,0 +1,82 @@ +use biome_analyze::{ + context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic, RuleSource, RuleSourceKind, +}; +use biome_console::markup; +use biome_js_syntax::{AnyJsExpression, JsCallExpression, JsSyntaxToken, TextRange}; + +declare_rule! { + /// Disallow focused tests. + /// + /// Disabled test are useful when developing and debugging, because it forces the test suite to run only certain tests. + /// + /// However, in pull/merge request, you usually want to run all the test suite. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// describe.only("foo", () => {}); + /// ``` + /// + /// ```js,expect_diagnostic + /// test.only("foo", () => {}); + /// ``` + pub(crate) NoFocusedTests { + version: "next", + name: "noFocusedTests", + recommended: true, + source: RuleSource::EslintJest("no-focused-tests"), + source_kind: RuleSourceKind::Inspired, + } +} + +impl Rule for NoFocusedTests { + type Query = Ast; + type State = TextRange; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + + if node.is_test_call_expression().ok()? { + let callee = node.callee().ok()?; + if callee.contains_a_test_pattern().ok()? { + let function_name = get_function_name(&callee)?; + + if function_name.text_trimmed() == "only" { + return Some(function_name.text_trimmed_range()); + } + } + } + + None + } + + fn diagnostic(_: &RuleContext, range: &Self::State) -> Option { + Some( + RuleDiagnostic::new( + rule_category!(), + range, + markup! { + "Don't focus the test." + }, + ) + .note("This is likely a change done during debugging or implementation phases, but it's unlikely what you want in production.") + .note("Remove it.") + ) + } +} + +fn get_function_name(callee: &AnyJsExpression) -> Option { + match callee { + AnyJsExpression::JsStaticMemberExpression(node) => { + let member = node.member().ok()?; + let member = member.as_js_name()?; + member.value_token().ok() + } + AnyJsExpression::JsIdentifierExpression(node) => node.name().ok()?.value_token().ok(), + _ => None, + } +} diff --git a/crates/biome_js_analyze/src/analyzers/nursery/no_skipped_tests.rs b/crates/biome_js_analyze/src/analyzers/nursery/no_skipped_tests.rs new file mode 100644 index 000000000000..6670ac460743 --- /dev/null +++ b/crates/biome_js_analyze/src/analyzers/nursery/no_skipped_tests.rs @@ -0,0 +1,91 @@ +use biome_analyze::{ + context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic, RuleSource, RuleSourceKind, +}; +use biome_console::markup; +use biome_js_syntax::{AnyJsExpression, JsCallExpression, JsSyntaxToken}; +use biome_rowan::TextRange; + +declare_rule! { + /// Disallow disabled tests. + /// + /// Disabled test are useful when developing and debugging, although they should not be committed in production. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// describe.skip("test", () => {}); + /// ``` + /// + /// ```js,expect_diagnostic + /// test.skip("test", () => {}); + /// ``` + /// + /// ## Valid + /// + /// ```js + /// test.only("test", () => {}); + /// test("test", () => {}); + /// ``` + /// + pub(crate) NoSkippedTests { + version: "next", + name: "noSkippedTests", + recommended: false, + source: RuleSource::EslintJest("no-disabled-tests"), + source_kind: RuleSourceKind::Inspired, + } +} + +const FUNCTION_NAMES: [&str; 4] = ["skip", "xdescribe", "xit", "xtest"]; + +impl Rule for NoSkippedTests { + type Query = Ast; + type State = TextRange; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + + if node.is_test_call_expression().ok()? { + let callee = node.callee().ok()?; + if callee.contains_a_test_pattern().ok()? { + let function_name = get_function_name(&callee)?; + + if FUNCTION_NAMES.contains(&function_name.text_trimmed()) { + return Some(function_name.text_trimmed_range()); + } + } + } + + None + } + + fn diagnostic(_: &RuleContext, range: &Self::State) -> Option { + Some( + RuleDiagnostic::new( + rule_category!(), + range, + markup! { + "Don't disable tests." + }, + ) + .note("Disabling tests is useful when debugging or creating placeholder while working.") + .note("If this is intentional, and you want to commit a disabled test, add a suppression comment.") + ) + } +} + +fn get_function_name(callee: &AnyJsExpression) -> Option { + match callee { + AnyJsExpression::JsStaticMemberExpression(node) => { + let member = node.member().ok()?; + let member = member.as_js_name()?; + member.value_token().ok() + } + AnyJsExpression::JsIdentifierExpression(node) => node.name().ok()?.value_token().ok(), + _ => None, + } +} diff --git a/crates/biome_js_analyze/src/lib.rs b/crates/biome_js_analyze/src/lib.rs index d138a39feb6d..fd782e60b0aa 100644 --- a/crates/biome_js_analyze/src/lib.rs +++ b/crates/biome_js_analyze/src/lib.rs @@ -243,17 +243,7 @@ mod tests { String::from_utf8(buffer).unwrap() } - const SOURCE: &str = r#" - import { useEffect } from "react"; - function MyComponent7() { - let someObj = getObj(); - useEffect(() => { - console.log( - someObj - .name - ); - }, [someObj]); -} + const SOURCE: &str = r#"xdescribe('foo', () => {}); "#; // const SOURCE: &str = r#"document.querySelector("foo").value = document.querySelector("foo").value // @@ -268,7 +258,7 @@ mod tests { closure_index: Some(0), dependencies_index: Some(1), }; - let rule_filter = RuleFilter::Rule("correctness", "useExhaustiveDependencies"); + let rule_filter = RuleFilter::Rule("nursery", "noDisabledTests"); options.configuration.rules.push_rule( RuleKey::new("nursery", "useHookAtTopLevel"), RuleOptions::new(HooksOptions { hooks: vec![hook] }), diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFocusedTests/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/noFocusedTests/invalid.js new file mode 100644 index 000000000000..4493a5628caf --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFocusedTests/invalid.js @@ -0,0 +1,5 @@ +describe.only("test", () => {}); +it.only("test", () => {}); +test.only("test", () => {}); +fdescribe('foo', () => {}); +fit('foo', () => {}); diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFocusedTests/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noFocusedTests/invalid.js.snap new file mode 100644 index 000000000000..f2619caef69f --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFocusedTests/invalid.js.snap @@ -0,0 +1,70 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.js +--- +# Input +```jsx +describe.only("test", () => {}); +it.only("test", () => {}); +test.only("test", () => {}); +fdescribe('foo', () => {}); +fit('foo', () => {}); + +``` + +# Diagnostics +``` +invalid.js:1:10 lint/nursery/noFocusedTests ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't focus the test. + + > 1 │ describe.only("test", () => {}); + │ ^^^^ + 2 │ it.only("test", () => {}); + 3 │ test.only("test", () => {}); + + i This is likely a change done during debugging or implementation phases, but it's unlikely what you want in production. + + i Remove it. + + +``` + +``` +invalid.js:2:4 lint/nursery/noFocusedTests ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't focus the test. + + 1 │ describe.only("test", () => {}); + > 2 │ it.only("test", () => {}); + │ ^^^^ + 3 │ test.only("test", () => {}); + 4 │ fdescribe('foo', () => {}); + + i This is likely a change done during debugging or implementation phases, but it's unlikely what you want in production. + + i Remove it. + + +``` + +``` +invalid.js:3:6 lint/nursery/noFocusedTests ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't focus the test. + + 1 │ describe.only("test", () => {}); + 2 │ it.only("test", () => {}); + > 3 │ test.only("test", () => {}); + │ ^^^^ + 4 │ fdescribe('foo', () => {}); + 5 │ fit('foo', () => {}); + + i This is likely a change done during debugging or implementation phases, but it's unlikely what you want in production. + + i Remove it. + + +``` + + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFocusedTests/valid.js b/crates/biome_js_analyze/tests/specs/nursery/noFocusedTests/valid.js new file mode 100644 index 000000000000..f0e2766e2c47 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFocusedTests/valid.js @@ -0,0 +1,5 @@ +describe.skip("test", () => {}); +it.skip("test", () => {}); +test.skip("test", () => {}); +fdescribe('foo', () => {}); +fit('foo', () => {}); diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFocusedTests/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noFocusedTests/valid.js.snap new file mode 100644 index 000000000000..6636f4c03edc --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFocusedTests/valid.js.snap @@ -0,0 +1,15 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.js +--- +# Input +```jsx +describe.skip("test", () => {}); +it.skip("test", () => {}); +test.skip("test", () => {}); +fdescribe('foo', () => {}); +fit('foo', () => {}); + +``` + + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noSkippedTests/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/noSkippedTests/invalid.js new file mode 100644 index 000000000000..60cbdc8507e8 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noSkippedTests/invalid.js @@ -0,0 +1,6 @@ +describe.skip("test", () => {}); +it.skip("test", () => {}); +test.skip("test", () => {}); +xdescribe('foo', () => {}); +xit('foo', () => {}); +xtest('foo', () => {}); diff --git a/crates/biome_js_analyze/tests/specs/nursery/noSkippedTests/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noSkippedTests/invalid.js.snap new file mode 100644 index 000000000000..319a0b3260e5 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noSkippedTests/invalid.js.snap @@ -0,0 +1,127 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.js +--- +# Input +```jsx +describe.skip("test", () => {}); +it.skip("test", () => {}); +test.skip("test", () => {}); +xdescribe('foo', () => {}); +xit('foo', () => {}); +xtest('foo', () => {}); + +``` + +# Diagnostics +``` +invalid.js:1:10 lint/nursery/noSkippedTests ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't disable tests. + + > 1 │ describe.skip("test", () => {}); + │ ^^^^ + 2 │ it.skip("test", () => {}); + 3 │ test.skip("test", () => {}); + + i Disabling tests is useful when debugging or creating placeholder while working. + + i If this is intentional, and you want to commit a disabled test, add a suppression comment. + + +``` + +``` +invalid.js:2:4 lint/nursery/noSkippedTests ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't disable tests. + + 1 │ describe.skip("test", () => {}); + > 2 │ it.skip("test", () => {}); + │ ^^^^ + 3 │ test.skip("test", () => {}); + 4 │ xdescribe('foo', () => {}); + + i Disabling tests is useful when debugging or creating placeholder while working. + + i If this is intentional, and you want to commit a disabled test, add a suppression comment. + + +``` + +``` +invalid.js:3:6 lint/nursery/noSkippedTests ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't disable tests. + + 1 │ describe.skip("test", () => {}); + 2 │ it.skip("test", () => {}); + > 3 │ test.skip("test", () => {}); + │ ^^^^ + 4 │ xdescribe('foo', () => {}); + 5 │ xit('foo', () => {}); + + i Disabling tests is useful when debugging or creating placeholder while working. + + i If this is intentional, and you want to commit a disabled test, add a suppression comment. + + +``` + +``` +invalid.js:4:1 lint/nursery/noSkippedTests ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't disable tests. + + 2 │ it.skip("test", () => {}); + 3 │ test.skip("test", () => {}); + > 4 │ xdescribe('foo', () => {}); + │ ^^^^^^^^^ + 5 │ xit('foo', () => {}); + 6 │ xtest('foo', () => {}); + + i Disabling tests is useful when debugging or creating placeholder while working. + + i If this is intentional, and you want to commit a disabled test, add a suppression comment. + + +``` + +``` +invalid.js:5:1 lint/nursery/noSkippedTests ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't disable tests. + + 3 │ test.skip("test", () => {}); + 4 │ xdescribe('foo', () => {}); + > 5 │ xit('foo', () => {}); + │ ^^^ + 6 │ xtest('foo', () => {}); + 7 │ + + i Disabling tests is useful when debugging or creating placeholder while working. + + i If this is intentional, and you want to commit a disabled test, add a suppression comment. + + +``` + +``` +invalid.js:6:1 lint/nursery/noSkippedTests ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't disable tests. + + 4 │ xdescribe('foo', () => {}); + 5 │ xit('foo', () => {}); + > 6 │ xtest('foo', () => {}); + │ ^^^^^ + 7 │ + + i Disabling tests is useful when debugging or creating placeholder while working. + + i If this is intentional, and you want to commit a disabled test, add a suppression comment. + + +``` + + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noSkippedTests/valid.js b/crates/biome_js_analyze/tests/specs/nursery/noSkippedTests/valid.js new file mode 100644 index 000000000000..c1eedbb5ca00 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noSkippedTests/valid.js @@ -0,0 +1,7 @@ +describe('foo', () => {}); +it('foo', () => {}); +test('foo', () => {}); + +describe.only('bar', () => {}); +it.only('bar', () => {}); +test.only('bar', () => {}); diff --git a/crates/biome_js_analyze/tests/specs/nursery/noSkippedTests/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noSkippedTests/valid.js.snap new file mode 100644 index 000000000000..57f03a6cdc70 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noSkippedTests/valid.js.snap @@ -0,0 +1,17 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.js +--- +# Input +```jsx +describe('foo', () => {}); +it('foo', () => {}); +test('foo', () => {}); + +describe.only('bar', () => {}); +it.only('bar', () => {}); +test.only('bar', () => {}); + +``` + + diff --git a/crates/biome_js_formatter/src/js/bindings/parameters.rs b/crates/biome_js_formatter/src/js/bindings/parameters.rs index 984b9257132d..28674c65dacc 100644 --- a/crates/biome_js_formatter/src/js/bindings/parameters.rs +++ b/crates/biome_js_formatter/src/js/bindings/parameters.rs @@ -3,11 +3,10 @@ use biome_formatter::{write, CstFormatContext}; use crate::js::expressions::arrow_function_expression::can_avoid_parentheses; use crate::js::lists::parameter_list::FormatJsAnyParameterList; -use crate::utils::test_call::is_test_call_argument; use biome_js_syntax::parameter_ext::{AnyJsParameterList, AnyParameter}; use biome_js_syntax::{ - AnyJsBinding, AnyJsBindingPattern, AnyJsConstructorParameter, AnyJsExpression, - AnyJsFormalParameter, AnyJsParameter, AnyTsType, JsArrowFunctionExpression, + is_test_call_argument, AnyJsBinding, AnyJsBindingPattern, AnyJsConstructorParameter, + AnyJsExpression, AnyJsFormalParameter, AnyJsParameter, AnyTsType, JsArrowFunctionExpression, JsConstructorParameters, JsParameters, JsSyntaxNode, JsSyntaxToken, }; use biome_rowan::{declare_node_union, AstNode, SyntaxResult}; diff --git a/crates/biome_js_formatter/src/js/expressions/arrow_function_expression.rs b/crates/biome_js_formatter/src/js/expressions/arrow_function_expression.rs index 2b8a3ca07359..2e650866e6ec 100644 --- a/crates/biome_js_formatter/src/js/expressions/arrow_function_expression.rs +++ b/crates/biome_js_formatter/src/js/expressions/arrow_function_expression.rs @@ -12,12 +12,11 @@ use crate::parentheses::{ update_or_lower_expression_needs_parentheses, AnyJsExpressionLeftSide, NeedsParentheses, }; use crate::utils::function_body::{FormatMaybeCachedFunctionBody, FunctionBodyCacheMode}; -use crate::utils::test_call::is_test_call_argument; use crate::utils::{resolve_left_most_expression, AssignmentLikeLayout}; use biome_js_syntax::{ - AnyJsArrowFunctionParameters, AnyJsBindingPattern, AnyJsExpression, AnyJsFormalParameter, - AnyJsFunctionBody, AnyJsParameter, AnyJsTemplateElement, JsArrowFunctionExpression, - JsFormalParameter, JsSyntaxKind, JsSyntaxNode, JsTemplateExpression, + is_test_call_argument, AnyJsArrowFunctionParameters, AnyJsBindingPattern, AnyJsExpression, + AnyJsFormalParameter, AnyJsFunctionBody, AnyJsParameter, AnyJsTemplateElement, + JsArrowFunctionExpression, JsFormalParameter, JsSyntaxKind, JsSyntaxNode, JsTemplateExpression, }; use biome_rowan::{SyntaxNodeOptionExt, SyntaxResult}; diff --git a/crates/biome_js_formatter/src/js/expressions/call_arguments.rs b/crates/biome_js_formatter/src/js/expressions/call_arguments.rs index 7b91acad70c7..ab3aabbc8773 100644 --- a/crates/biome_js_formatter/src/js/expressions/call_arguments.rs +++ b/crates/biome_js_formatter/src/js/expressions/call_arguments.rs @@ -8,7 +8,6 @@ use crate::js::lists::array_element_list::can_concisely_print_array_list; use crate::prelude::*; use crate::utils::function_body::FunctionBodyCacheMode; use crate::utils::member_chain::SimpleArgument; -use crate::utils::test_call::is_test_call_expression; use crate::utils::{is_long_curried_call, write_arguments_multi_line}; use biome_formatter::{format_args, format_element, write, VecBuffer}; use biome_js_syntax::{ @@ -50,7 +49,7 @@ impl FormatNodeRule for FormatJsCallArguments { .map_or((Ok(false), Ok(false)), |call| { ( is_commonjs_or_amd_call(node, call), - is_test_call_expression(call), + call.is_test_call_expression(), ) }); diff --git a/crates/biome_js_formatter/src/js/expressions/template_expression.rs b/crates/biome_js_formatter/src/js/expressions/template_expression.rs index 4892c1492fb3..f96faa46f043 100644 --- a/crates/biome_js_formatter/src/js/expressions/template_expression.rs +++ b/crates/biome_js_formatter/src/js/expressions/template_expression.rs @@ -4,7 +4,6 @@ use biome_formatter::write; use crate::js::expressions::static_member_expression::member_chain_callee_needs_parens; use crate::js::lists::template_element_list::FormatJsTemplateElementListOptions; use crate::parentheses::NeedsParentheses; -use crate::utils::test_call::is_test_each_pattern; use biome_js_syntax::{AnyJsExpression, JsSyntaxNode, JsTemplateExpression, TsTemplateLiteralType}; use biome_js_syntax::{JsSyntaxToken, TsTypeArguments}; use biome_rowan::{declare_node_union, SyntaxResult}; @@ -69,7 +68,7 @@ impl AnyJsTemplate { fn write_elements(&self, f: &mut JsFormatter) -> FormatResult<()> { match self { AnyJsTemplate::JsTemplateExpression(template) => { - let is_test_each_pattern = is_test_each_pattern(template); + let is_test_each_pattern = template.is_test_each_pattern(); let options = FormatJsTemplateElementListOptions { is_test_each_pattern, }; diff --git a/crates/biome_js_formatter/src/utils/member_chain/mod.rs b/crates/biome_js_formatter/src/utils/member_chain/mod.rs index 47404c92629b..1a8befd78967 100644 --- a/crates/biome_js_formatter/src/utils/member_chain/mod.rs +++ b/crates/biome_js_formatter/src/utils/member_chain/mod.rs @@ -114,7 +114,6 @@ use crate::utils::member_chain::groups::{ MemberChainGroup, MemberChainGroupsBuilder, TailChainGroups, }; pub use crate::utils::member_chain::simple_argument::SimpleArgument; -use crate::utils::test_call::is_test_call_expression; use crate::JsLabels; use biome_formatter::{write, Buffer}; use biome_js_syntax::{ @@ -363,7 +362,7 @@ impl Format for MemberChain { if self.tail.len() <= 1 && !has_comments { return if is_long_curried_call(Some(&self.root)) { write!(f, [format_one_line]) - } else if is_test_call_expression(&self.root)? && self.head.members().len() >= 2 { + } else if self.root.is_test_call_expression()? && self.head.members().len() >= 2 { write!(f, [self.head, soft_line_indent_or_space(&self.tail)]) } else { write!(f, [group(&format_one_line)]) diff --git a/crates/biome_js_formatter/src/utils/mod.rs b/crates/biome_js_formatter/src/utils/mod.rs index 704ed349a4d3..f9b0a0381dc4 100644 --- a/crates/biome_js_formatter/src/utils/mod.rs +++ b/crates/biome_js_formatter/src/utils/mod.rs @@ -14,7 +14,6 @@ mod object_like; mod object_pattern_like; #[cfg(test)] mod quickcheck_utils; -pub(crate) mod test_call; pub(crate) mod test_each_template; mod typescript; diff --git a/crates/biome_js_formatter/src/utils/test_call.rs b/crates/biome_js_formatter/src/utils/test_call.rs deleted file mode 100644 index b7f3e5146f78..000000000000 --- a/crates/biome_js_formatter/src/utils/test_call.rs +++ /dev/null @@ -1,697 +0,0 @@ -use crate::prelude::*; -use biome_js_syntax::{ - AnyJsArrowFunctionParameters, AnyJsCallArgument, AnyJsExpression, AnyJsFunctionBody, - AnyJsLiteralExpression, AnyJsName, AnyJsTemplateElement, JsCallArgumentList, JsCallArguments, - JsCallExpression, JsSyntaxNode, JsTemplateExpression, -}; -use biome_rowan::{SyntaxResult, TokenText}; - -/// Returns `Ok(true)` if `maybe_argument` is an argument of a [test call expression](is_test_call_expression). -pub(crate) fn is_test_call_argument(maybe_argument: &JsSyntaxNode) -> SyntaxResult { - let call_expression = maybe_argument - .parent() - .and_then(JsCallArgumentList::cast) - .and_then(|args| args.syntax().grand_parent()) - .and_then(JsCallExpression::cast); - - call_expression.map_or(Ok(false), |call| is_test_call_expression(&call)) -} - -/// This is a specialised function that checks if the current [call expression] -/// resembles a call expression usually used by a testing frameworks. -/// -/// If the [call expression] matches the criteria, a different formatting is applied. -/// -/// To evaluable the eligibility of a [call expression] to be a test framework like, -/// we need to check its [callee] and its [arguments]. -/// -/// 1. The [callee] must contain a name or a chain of names that belongs to the -/// test frameworks, for example: `test()`, `test.only()`, etc. -/// 2. The [arguments] should be at the least 2 -/// 3. The first argument has to be a string literal -/// 4. The third argument, if present, has to be a number literal -/// 5. The second argument has to be an [arrow function expression] or [function expression] -/// 6. Both function must have zero or one parameters -/// -/// [call expression]: crate::biome_js_syntax::JsCallExpression -/// [callee]: crate::biome_js_syntax::AnyJsExpression -/// [arguments]: crate::biome_js_syntax::JsCallArgumentList -/// [arrow function expression]: crate::biome_js_syntax::JsArrowFunctionExpression -/// [function expression]: crate::biome_js_syntax::JsCallArgumentList -pub(crate) fn is_test_call_expression(call_expression: &JsCallExpression) -> SyntaxResult { - use AnyJsExpression::*; - - let callee = call_expression.callee()?; - let arguments = call_expression.arguments()?; - - let mut args = arguments.args().iter(); - - match (args.next(), args.next(), args.next()) { - (Some(Ok(argument)), None, None) if arguments.args().len() == 1 => { - if is_angular_test_wrapper(&call_expression.clone().into()) - && call_expression - .parent::() - .and_then(|arguments_list| arguments_list.parent::()) - .and_then(|arguments| arguments.parent::()) - .map_or(Ok(false), |parent| is_test_call_expression(&parent))? - { - return Ok(matches!( - argument, - AnyJsCallArgument::AnyJsExpression( - JsArrowFunctionExpression(_) | JsFunctionExpression(_) - ) - )); - } - - if is_unit_test_set_up_callee(&callee) { - return Ok(argument - .as_any_js_expression() - .map_or(false, is_angular_test_wrapper)); - } - - Ok(false) - } - - // it("description", ..) - ( - Some(Ok(AnyJsCallArgument::AnyJsExpression( - JsTemplateExpression(_) - | AnyJsLiteralExpression(self::AnyJsLiteralExpression::JsStringLiteralExpression(_)), - ))), - Some(Ok(second)), - third, - ) if arguments.args().len() <= 3 && contains_a_test_pattern(&callee)? => { - // it('name', callback, duration) - if !matches!( - third, - None | Some(Ok(AnyJsCallArgument::AnyJsExpression( - AnyJsLiteralExpression( - self::AnyJsLiteralExpression::JsNumberLiteralExpression(_) - ) - ))) - ) { - return Ok(false); - } - - if second - .as_any_js_expression() - .map_or(false, is_angular_test_wrapper) - { - return Ok(true); - } - - let (parameters, has_block_body) = match second { - AnyJsCallArgument::AnyJsExpression(JsFunctionExpression(function)) => ( - function - .parameters() - .map(AnyJsArrowFunctionParameters::from), - true, - ), - AnyJsCallArgument::AnyJsExpression(JsArrowFunctionExpression(arrow)) => ( - arrow.parameters(), - arrow.body().map_or(false, |body| { - matches!(body, AnyJsFunctionBody::JsFunctionBody(_)) - }), - ), - _ => return Ok(false), - }; - - Ok(arguments.args().len() == 2 || (parameters?.len() <= 1 && has_block_body)) - } - _ => Ok(false), - } -} - -/// Note: `inject` is used in AngularJS 1.x, `async` and `fakeAsync` in -/// Angular 2+, although `async` is deprecated and replaced by `waitForAsync` -/// since Angular 12. -/// -/// example: https://docs.angularjs.org/guide/unit-testing#using-beforeall- -/// -/// @param {CallExpression} node -/// @returns {boolean} -/// -fn is_angular_test_wrapper(expression: &AnyJsExpression) -> bool { - use AnyJsExpression::*; - match expression { - JsCallExpression(call_expression) => match call_expression.callee() { - Ok(JsIdentifierExpression(identifier)) => identifier - .name() - .and_then(|name| name.value_token()) - .map_or(false, |name| { - matches!( - name.text_trimmed(), - "async" | "inject" | "fakeAsync" | "waitForAsync" - ) - }), - _ => false, - }, - _ => false, - } -} - -/// Tests if the callee is a `beforeEach`, `beforeAll`, `afterEach` or `afterAll` identifier -/// that is commonly used in test frameworks. -fn is_unit_test_set_up_callee(callee: &AnyJsExpression) -> bool { - match callee { - AnyJsExpression::JsIdentifierExpression(identifier) => identifier - .name() - .and_then(|name| name.value_token()) - .map_or(false, |name| { - matches!( - name.text_trimmed(), - "beforeEach" | "beforeAll" | "afterEach" | "afterAll" - ) - }), - _ => false, - } -} - -pub(crate) fn is_test_each_pattern(template: &JsTemplateExpression) -> bool { - is_test_each_pattern_callee(template) && is_test_each_pattern_elements(template) -} - -fn is_test_each_pattern_elements(template: &JsTemplateExpression) -> bool { - let mut iter = template.elements().into_iter(); - - // the table must have a header as JsTemplateChunkElement - // e.g. a | b | expected - if !matches!( - iter.next(), - Some(AnyJsTemplateElement::JsTemplateChunkElement(_)) - ) { - return false; - } - - // Guarding against skipped token trivia on elements that we remove. - // Because that would result in the skipped token trivia being emitted before the template. - for element in template.elements() { - if let AnyJsTemplateElement::JsTemplateChunkElement(element) = element { - if let Some(leading_trivia) = element.syntax().first_leading_trivia() { - if leading_trivia.has_skipped() { - return false; - } - } - } - } - - true -} - -/// This function checks if a call expressions has one of the following members: -/// - `describe.each` -/// - `describe.only.each` -/// - `describe.skip.each` -/// - `test.concurrent.each` -/// - `test.concurrent.only.each` -/// - `test.concurrent.skip.each` -/// - `test.each` -/// - `test.only.each` -/// - `test.skip.each` -/// - `test.failing.each` -/// - `it.concurrent.each` -/// - `it.concurrent.only.each` -/// - `it.concurrent.skip.each` -/// - `it.each` -/// - `it.only.each` -/// - `it.skip.each` -/// - `it.failing.each` -/// -/// - `xdescribe.each` -/// - `xdescribe.only.each` -/// - `xdescribe.skip.each` -/// - `xtest.concurrent.each` -/// - `xtest.concurrent.only.each` -/// - `xtest.concurrent.skip.each` -/// - `xtest.each` -/// - `xtest.only.each` -/// - `xtest.skip.each` -/// - `xtest.failing.each` -/// - `xit.concurrent.each` -/// - `xit.concurrent.only.each` -/// - `xit.concurrent.skip.each` -/// - `xit.each` -/// - `xit.only.each` -/// - `xit.skip.each` -/// - `xit.failing.each` -/// -/// - `fdescribe.each` -/// - `fdescribe.only.each` -/// - `fdescribe.skip.each` -/// - `ftest.concurrent.each` -/// - `ftest.concurrent.only.each` -/// - `ftest.concurrent.skip.each` -/// - `ftest.each` -/// - `ftest.only.each` -/// - `ftest.skip.each` -/// - `ftest.failing.each` -/// - `fit.concurrent.each` -/// - `fit.concurrent.only.each` -/// - `fit.concurrent.skip.each` -/// - `fit.each` -/// - `fit.only.each` -/// - `fit.skip.each` -/// - `xit.failing.each` -/// -/// Based on this [article] -/// -/// [article]: https://craftinginterpreters.com/scanning-on-demand.html#tries-and-state-machines -fn is_test_each_pattern_callee(template: &JsTemplateExpression) -> bool { - if let Some(tag) = template.tag() { - let mut members = CalleeNamesIterator::new(tag); - - let texts: [Option; 5] = [ - members.next(), - members.next(), - members.next(), - members.next(), - members.next(), - ]; - - let mut rev = texts.iter().rev().flatten(); - - let first = rev.next().map(|t| t.text()); - let second = rev.next().map(|t| t.text()); - let third = rev.next().map(|t| t.text()); - let fourth = rev.next().map(|t| t.text()); - let fifth = rev.next().map(|t| t.text()); - - match first { - Some("describe" | "xdescribe" | "fdescribe") => match second { - Some("each") => third.is_none(), - Some("skip" | "only") => match third { - Some("each") => fourth.is_none(), - _ => false, - }, - _ => false, - }, - Some("test" | "xtest" | "ftest" | "it" | "xit" | "fit") => match second { - Some("each") => third.is_none(), - Some("skip" | "only" | "failing") => match third { - Some("each") => fourth.is_none(), - _ => false, - }, - Some("concurrent") => match third { - Some("each") => fourth.is_none(), - Some("only" | "skip") => match fourth { - Some("each") => fifth.is_none(), - _ => false, - }, - _ => false, - }, - _ => false, - }, - _ => false, - } - } else { - false - } -} - -/// This function checks if a call expressions has one of the following members: -/// - `it` -/// - `it.only` -/// - `it.skip` -/// - `describe` -/// - `describe.only` -/// - `describe.skip` -/// - `test` -/// - `test.only` -/// - `test.skip` -/// - `test.step` -/// - `test.describe` -/// - `test.describe.only` -/// - `test.describe.parallel` -/// - `test.describe.parallel.only` -/// - `test.describe.serial` -/// - `test.describe.serial.only` -/// - `skip` -/// - `xit` -/// - `xdescribe` -/// - `xtest` -/// - `fit` -/// - `fdescribe` -/// - `ftest` -/// -/// Based on this [article] -/// -/// [article]: https://craftinginterpreters.com/scanning-on-demand.html#tries-and-state-machines -fn contains_a_test_pattern(callee: &AnyJsExpression) -> SyntaxResult { - let mut members = CalleeNamesIterator::new(callee.clone()); - - let texts: [Option; 5] = [ - members.next(), - members.next(), - members.next(), - members.next(), - members.next(), - ]; - - let mut rev = texts.iter().rev().flatten(); - - let first = rev.next().map(|t| t.text()); - let second = rev.next().map(|t| t.text()); - let third = rev.next().map(|t| t.text()); - let fourth = rev.next().map(|t| t.text()); - let fifth = rev.next().map(|t| t.text()); - - Ok(match first { - Some("it" | "describe") => match second { - None => true, - Some("only" | "skip") => third.is_none(), - _ => false, - }, - Some("test") => match second { - None => true, - Some("only" | "skip" | "step") => third.is_none(), - Some("describe") => match third { - None => true, - Some("only") => fourth.is_none(), - Some("parallel" | "serial") => match fourth { - None => true, - Some("only") => fifth.is_none(), - _ => false, - }, - _ => false, - }, - _ => false, - }, - Some("skip" | "xit" | "xdescribe" | "xtest" | "fit" | "fdescribe" | "ftest") => true, - _ => false, - }) -} - -/// Iterator that returns the callee names in "top down order". -/// -/// # Examples -/// -/// ```javascript -/// it.only() -> [`only`, `it`] -/// ``` -struct CalleeNamesIterator { - next: Option, -} - -impl CalleeNamesIterator { - fn new(callee: AnyJsExpression) -> Self { - Self { next: Some(callee) } - } -} - -impl Iterator for CalleeNamesIterator { - type Item = TokenText; - - fn next(&mut self) -> Option { - use AnyJsExpression::*; - - let current = self.next.take()?; - - match current { - JsIdentifierExpression(identifier) => identifier - .name() - .and_then(|reference| reference.value_token()) - .ok() - .map(|value| value.token_text_trimmed()), - JsStaticMemberExpression(member_expression) => match member_expression.member() { - Ok(AnyJsName::JsName(name)) => { - self.next = member_expression.object().ok(); - name.value_token() - .ok() - .map(|name| name.token_text_trimmed()) - } - _ => None, - }, - _ => None, - } - } -} - -#[cfg(test)] -mod test { - use super::{contains_a_test_pattern, is_test_each_pattern_callee}; - use biome_js_parser::{parse, JsParserOptions}; - use biome_js_syntax::{JsCallExpression, JsFileSource, JsTemplateExpression}; - use biome_rowan::AstNodeList; - - fn extract_call_expression(src: &str) -> JsCallExpression { - let source_type = JsFileSource::js_module(); - let result = parse(src, source_type, JsParserOptions::default()); - let module = result - .tree() - .as_js_module() - .unwrap() - .items() - .first() - .unwrap(); - - module - .as_any_js_statement() - .unwrap() - .as_js_expression_statement() - .unwrap() - .expression() - .unwrap() - .as_js_call_expression() - .unwrap() - .clone() - } - - fn extract_template(src: &str) -> JsTemplateExpression { - let source_type = JsFileSource::js_module(); - let result = parse(src, source_type, JsParserOptions::default()); - let module = result - .tree() - .as_js_module() - .unwrap() - .items() - .first() - .unwrap(); - - module - .as_any_js_statement() - .unwrap() - .as_js_expression_statement() - .unwrap() - .expression() - .unwrap() - .as_js_template_expression() - .unwrap() - .clone() - } - - #[test] - fn matches_simple_call() { - let call_expression = extract_call_expression("test();"); - assert_eq!( - contains_a_test_pattern(&call_expression.callee().unwrap()), - Ok(true) - ); - - let call_expression = extract_call_expression("it();"); - assert_eq!( - contains_a_test_pattern(&call_expression.callee().unwrap()), - Ok(true) - ); - } - - #[test] - fn matches_static_member_expression() { - let call_expression = extract_call_expression("test.only();"); - assert_eq!( - contains_a_test_pattern(&call_expression.callee().unwrap()), - Ok(true) - ); - } - - #[test] - fn matches_static_member_expression_deep() { - let call_expression = extract_call_expression("test.describe.parallel.only();"); - assert_eq!( - contains_a_test_pattern(&call_expression.callee().unwrap()), - Ok(true) - ); - } - - #[test] - fn doesnt_static_member_expression_deep() { - let call_expression = extract_call_expression("test.describe.parallel.only.AHAHA();"); - assert_eq!( - contains_a_test_pattern(&call_expression.callee().unwrap()), - Ok(false) - ); - } - - #[test] - fn matches_simple_each() { - let template = extract_template("describe.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("test.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("it.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("xdescribe.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("xtest.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("xit.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("fdescribe.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("ftest.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("fit.each``"); - assert!(is_test_each_pattern_callee(&template)); - } - - #[test] - fn matches_skip_each() { - let template = extract_template("describe.skip.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("test.skip.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("it.skip.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("xdescribe.skip.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("xtest.skip.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("xit.skip.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("fdescribe.skip.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("ftest.skip.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("fit.skip.each``"); - assert!(is_test_each_pattern_callee(&template)); - } - - #[test] - fn matches_only_each() { - let template = extract_template("describe.only.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("test.only.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("it.only.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("xdescribe.only.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("xtest.only.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("xit.only.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("fdescribe.only.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("ftest.only.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("fit.only.each``"); - assert!(is_test_each_pattern_callee(&template)); - } - - #[test] - fn matches_failing_each() { - let template = extract_template("test.failing.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("it.failing.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("xtest.failing.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("xit.failing.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("ftest.failing.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("fit.failing.each``"); - assert!(is_test_each_pattern_callee(&template)); - } - - #[test] - fn matches_concurrent_each() { - let template = extract_template("test.concurrent.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("it.concurrent.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("xtest.concurrent.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("xit.concurrent.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("ftest.concurrent.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("fit.concurrent.each``"); - assert!(is_test_each_pattern_callee(&template)); - } - - #[test] - fn matches_concurrent_only_each() { - let template = extract_template("test.concurrent.only.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("it.concurrent.only.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("xtest.concurrent.only.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("xit.concurrent.only.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("ftest.concurrent.only.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("fit.concurrent.only.each``"); - assert!(is_test_each_pattern_callee(&template)); - } - - #[test] - fn matches_concurrent_skip_each() { - let template = extract_template("test.concurrent.skip.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("it.concurrent.skip.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("xtest.concurrent.skip.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("xit.concurrent.skip.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("ftest.concurrent.skip.each``"); - assert!(is_test_each_pattern_callee(&template)); - - let template = extract_template("fit.concurrent.skip.each``"); - assert!(is_test_each_pattern_callee(&template)); - } -} diff --git a/crates/biome_js_syntax/Cargo.toml b/crates/biome_js_syntax/Cargo.toml index e64202c5d3a1..0ff2997a81fd 100644 --- a/crates/biome_js_syntax/Cargo.toml +++ b/crates/biome_js_syntax/Cargo.toml @@ -19,6 +19,7 @@ serde = { version = "1.0.136", features = ["derive"], optional = tru [dev-dependencies] biome_js_factory = { path = "../biome_js_factory" } +biome_js_parser = { path = "../biome_js_parser" } [features] serde = ["dep:serde", "schemars", "biome_rowan/serde"] diff --git a/crates/biome_js_syntax/src/expr_ext.rs b/crates/biome_js_syntax/src/expr_ext.rs index 7628562b7b35..3edadf17c392 100644 --- a/crates/biome_js_syntax/src/expr_ext.rs +++ b/crates/biome_js_syntax/src/expr_ext.rs @@ -2,14 +2,15 @@ use crate::numbers::parse_js_number; use crate::static_value::StaticValue; use crate::{ - inner_string_text, AnyJsClassMemberName, AnyJsExpression, AnyJsLiteralExpression, - AnyJsObjectMemberName, AnyJsTemplateElement, JsArrayExpression, JsArrayHole, - JsAssignmentExpression, JsBinaryExpression, JsCallExpression, JsComputedMemberAssignment, - JsComputedMemberExpression, JsLiteralMemberName, JsLogicalExpression, JsNewExpression, - JsNumberLiteralExpression, JsObjectExpression, JsPostUpdateExpression, JsReferenceIdentifier, - JsRegexLiteralExpression, JsStaticMemberExpression, JsStringLiteralExpression, JsSyntaxToken, - JsTemplateChunkElement, JsTemplateExpression, JsUnaryExpression, OperatorPrecedence, - TsStringLiteralType, T, + inner_string_text, AnyJsArrowFunctionParameters, AnyJsCallArgument, AnyJsClassMemberName, + AnyJsExpression, AnyJsFunctionBody, AnyJsLiteralExpression, AnyJsName, AnyJsObjectMemberName, + AnyJsTemplateElement, JsArrayExpression, JsArrayHole, JsAssignmentExpression, + JsBinaryExpression, JsCallArgumentList, JsCallArguments, JsCallExpression, + JsComputedMemberAssignment, JsComputedMemberExpression, JsLiteralMemberName, + JsLogicalExpression, JsNewExpression, JsNumberLiteralExpression, JsObjectExpression, + JsPostUpdateExpression, JsReferenceIdentifier, JsRegexLiteralExpression, + JsStaticMemberExpression, JsStringLiteralExpression, JsSyntaxToken, JsTemplateChunkElement, + JsTemplateExpression, JsUnaryExpression, OperatorPrecedence, TsStringLiteralType, T, }; use crate::{JsPreUpdateExpression, JsSyntaxKind::*}; use biome_rowan::{ @@ -653,6 +654,147 @@ impl JsTemplateExpression { self.syntax().text_range().end(), )) } + + pub fn is_test_each_pattern(&self) -> bool { + self.is_test_each_pattern_callee() && self.is_test_each_pattern_elements() + } + + /// This function checks if a call expressions has one of the following members: + /// - `describe.each` + /// - `describe.only.each` + /// - `describe.skip.each` + /// - `test.concurrent.each` + /// - `test.concurrent.only.each` + /// - `test.concurrent.skip.each` + /// - `test.each` + /// - `test.only.each` + /// - `test.skip.each` + /// - `test.failing.each` + /// - `it.concurrent.each` + /// - `it.concurrent.only.each` + /// - `it.concurrent.skip.each` + /// - `it.each` + /// - `it.only.each` + /// - `it.skip.each` + /// - `it.failing.each` + /// + /// - `xdescribe.each` + /// - `xdescribe.only.each` + /// - `xdescribe.skip.each` + /// - `xtest.concurrent.each` + /// - `xtest.concurrent.only.each` + /// - `xtest.concurrent.skip.each` + /// - `xtest.each` + /// - `xtest.only.each` + /// - `xtest.skip.each` + /// - `xtest.failing.each` + /// - `xit.concurrent.each` + /// - `xit.concurrent.only.each` + /// - `xit.concurrent.skip.each` + /// - `xit.each` + /// - `xit.only.each` + /// - `xit.skip.each` + /// - `xit.failing.each` + /// + /// - `fdescribe.each` + /// - `fdescribe.only.each` + /// - `fdescribe.skip.each` + /// - `ftest.concurrent.each` + /// - `ftest.concurrent.only.each` + /// - `ftest.concurrent.skip.each` + /// - `ftest.each` + /// - `ftest.only.each` + /// - `ftest.skip.each` + /// - `ftest.failing.each` + /// - `fit.concurrent.each` + /// - `fit.concurrent.only.each` + /// - `fit.concurrent.skip.each` + /// - `fit.each` + /// - `fit.only.each` + /// - `fit.skip.each` + /// - `xit.failing.each` + /// + /// Based on this [article] + /// + /// [article]: https://craftinginterpreters.com/scanning-on-demand.html#tries-and-state-machines + pub fn is_test_each_pattern_callee(&self) -> bool { + if let Some(tag) = self.tag() { + let mut members = CalleeNamesIterator::new(tag); + + let texts: [Option; 5] = [ + members.next(), + members.next(), + members.next(), + members.next(), + members.next(), + ]; + + let mut rev = texts.iter().rev().flatten(); + + let first = rev.next().map(|t| t.text()); + let second = rev.next().map(|t| t.text()); + let third = rev.next().map(|t| t.text()); + let fourth = rev.next().map(|t| t.text()); + let fifth = rev.next().map(|t| t.text()); + + match first { + Some("describe" | "xdescribe" | "fdescribe") => match second { + Some("each") => third.is_none(), + Some("skip" | "only") => match third { + Some("each") => fourth.is_none(), + _ => false, + }, + _ => false, + }, + Some("test" | "xtest" | "ftest" | "it" | "xit" | "fit") => match second { + Some("each") => third.is_none(), + Some("skip" | "only" | "failing") => match third { + Some("each") => fourth.is_none(), + _ => false, + }, + Some("concurrent") => match third { + Some("each") => fourth.is_none(), + Some("only" | "skip") => match fourth { + Some("each") => fifth.is_none(), + _ => false, + }, + _ => false, + }, + _ => false, + }, + _ => false, + } + } else { + false + } + } + + fn is_test_each_pattern_elements(&self) -> bool { + let mut iter = self.elements().into_iter(); + + // the table must have a header as JsTemplateChunkElement + // e.g. a | b | expected + if !matches!( + iter.next(), + Some(AnyJsTemplateElement::JsTemplateChunkElement(_)) + ) { + return false; + } + + // Guarding against skipped token trivia on elements that we remove. + // Because that would result in the skipped token trivia being emitted before the template. + for element in self.elements() { + if let AnyJsTemplateElement::JsTemplateChunkElement(element) = element { + if let Some(leading_trivia) = element.syntax().first_leading_trivia() { + if leading_trivia.has_skipped() { + return false; + } + } + } + } + + true + } } impl JsRegexLiteralExpression { @@ -816,6 +958,124 @@ impl AnyJsExpression { _ => None, } } + + /// This function checks if a call expressions has one of the following members: + /// - `it` + /// - `it.only` + /// - `it.skip` + /// - `describe` + /// - `describe.only` + /// - `describe.skip` + /// - `test` + /// - `test.only` + /// - `test.skip` + /// - `test.step` + /// - `test.describe` + /// - `test.describe.only` + /// - `test.describe.parallel` + /// - `test.describe.parallel.only` + /// - `test.describe.serial` + /// - `test.describe.serial.only` + /// - `skip` + /// - `xit` + /// - `xdescribe` + /// - `xtest` + /// - `fit` + /// - `fdescribe` + /// - `ftest` + /// + /// Based on this [article] + /// + /// [article]: https://craftinginterpreters.com/scanning-on-demand.html#tries-and-state-machines + pub fn contains_a_test_pattern(&self) -> SyntaxResult { + let mut members = CalleeNamesIterator::new(self.clone()); + + let texts: [Option; 5] = [ + members.next(), + members.next(), + members.next(), + members.next(), + members.next(), + ]; + + let mut rev = texts.iter().rev().flatten(); + + let first = rev.next().map(|t| t.text()); + let second = rev.next().map(|t| t.text()); + let third = rev.next().map(|t| t.text()); + let fourth = rev.next().map(|t| t.text()); + let fifth = rev.next().map(|t| t.text()); + + Ok(match first { + Some("it" | "describe") => match second { + None => true, + Some("only" | "skip") => third.is_none(), + _ => false, + }, + Some("test") => match second { + None => true, + Some("only" | "skip" | "step") => third.is_none(), + Some("describe") => match third { + None => true, + Some("only") => fourth.is_none(), + Some("parallel" | "serial") => match fourth { + None => true, + Some("only") => fifth.is_none(), + _ => false, + }, + _ => false, + }, + _ => false, + }, + Some("skip" | "xit" | "xdescribe" | "xtest" | "fit" | "fdescribe" | "ftest") => true, + _ => false, + }) + } +} + +/// Iterator that returns the callee names in "top down order". +/// +/// # Examples +/// +/// ```javascript +/// it.only() -> [`only`, `it`] +/// ``` +struct CalleeNamesIterator { + next: Option, +} + +impl CalleeNamesIterator { + fn new(callee: AnyJsExpression) -> Self { + Self { next: Some(callee) } + } +} + +impl Iterator for CalleeNamesIterator { + type Item = TokenText; + + fn next(&mut self) -> Option { + use AnyJsExpression::*; + + let current = self.next.take()?; + + match current { + JsIdentifierExpression(identifier) => identifier + .name() + .and_then(|reference| reference.value_token()) + .ok() + .map(|value| value.token_text_trimmed()), + JsStaticMemberExpression(member_expression) => match member_expression.member() { + Ok(AnyJsName::JsName(name)) => { + self.next = member_expression.object().ok(); + name.value_token() + .ok() + .map(|name| name.token_text_trimmed()) + } + _ => None, + }, + _ => None, + } + } } impl AnyJsLiteralExpression { @@ -1284,6 +1544,158 @@ impl JsCallExpression { .map_or(false, |it| it.has_name(name)) }) } + + /// This is a specialised function that checks if the current [call expression] + /// resembles a call expression usually used by a testing frameworks. + /// + /// If the [call expression] matches the criteria, a different formatting is applied. + /// + /// To evaluable the eligibility of a [call expression] to be a test framework like, + /// we need to check its [callee] and its [arguments]. + /// + /// 1. The [callee] must contain a name or a chain of names that belongs to the + /// test frameworks, for example: `test()`, `test.only()`, etc. + /// 2. The [arguments] should be at the least 2 + /// 3. The first argument has to be a string literal + /// 4. The third argument, if present, has to be a number literal + /// 5. The second argument has to be an [arrow function expression] or [function expression] + /// 6. Both function must have zero or one parameters + /// + /// [call expression]: crate::biome_js_syntax::JsCallExpression + /// [callee]: crate::biome_js_syntax::AnyJsExpression + /// [arguments]: crate::biome_js_syntax::JsCallArgumentList + /// [arrow function expression]: crate::biome_js_syntax::JsArrowFunctionExpression + /// [function expression]: crate::biome_js_syntax::JsCallArgumentList + pub fn is_test_call_expression(&self) -> SyntaxResult { + use AnyJsExpression::*; + + let callee = self.callee()?; + let arguments = self.arguments()?; + + let mut args = arguments.args().iter(); + + match (args.next(), args.next(), args.next()) { + (Some(Ok(argument)), None, None) if arguments.args().len() == 1 => { + if is_angular_test_wrapper(&self.clone().into()) + && self + .parent::() + .and_then(|arguments_list| arguments_list.parent::()) + .and_then(|arguments| arguments.parent::()) + .map_or(Ok(false), |parent| parent.is_test_call_expression())? + { + return Ok(matches!( + argument, + AnyJsCallArgument::AnyJsExpression( + JsArrowFunctionExpression(_) | JsFunctionExpression(_) + ) + )); + } + + if is_unit_test_set_up_callee(&callee) { + return Ok(argument + .as_any_js_expression() + .map_or(false, is_angular_test_wrapper)); + } + + Ok(false) + } + + // it("description", ..) + ( + Some(Ok(AnyJsCallArgument::AnyJsExpression( + JsTemplateExpression(_) + | AnyJsLiteralExpression( + self::AnyJsLiteralExpression::JsStringLiteralExpression(_), + ), + ))), + Some(Ok(second)), + third, + ) if arguments.args().len() <= 3 && callee.contains_a_test_pattern()? => { + // it('name', callback, duration) + if !matches!( + third, + None | Some(Ok(AnyJsCallArgument::AnyJsExpression( + AnyJsLiteralExpression( + self::AnyJsLiteralExpression::JsNumberLiteralExpression(_) + ) + ))) + ) { + return Ok(false); + } + + if second + .as_any_js_expression() + .map_or(false, is_angular_test_wrapper) + { + return Ok(true); + } + + let (parameters, has_block_body) = match second { + AnyJsCallArgument::AnyJsExpression(JsFunctionExpression(function)) => ( + function + .parameters() + .map(AnyJsArrowFunctionParameters::from), + true, + ), + AnyJsCallArgument::AnyJsExpression(JsArrowFunctionExpression(arrow)) => ( + arrow.parameters(), + arrow.body().map_or(false, |body| { + matches!(body, AnyJsFunctionBody::JsFunctionBody(_)) + }), + ), + _ => return Ok(false), + }; + + Ok(arguments.args().len() == 2 || (parameters?.len() <= 1 && has_block_body)) + } + _ => Ok(false), + } + } +} + +/// Note: `inject` is used in AngularJS 1.x, `async` and `fakeAsync` in +/// Angular 2+, although `async` is deprecated and replaced by `waitForAsync` +/// since Angular 12. +/// +/// example: https://docs.angularjs.org/guide/unit-testing#using-beforeall- +/// +/// @param {CallExpression} node +/// @returns {boolean} +/// +fn is_angular_test_wrapper(expression: &AnyJsExpression) -> bool { + use AnyJsExpression::*; + match expression { + JsCallExpression(call_expression) => match call_expression.callee() { + Ok(JsIdentifierExpression(identifier)) => identifier + .name() + .and_then(|name| name.value_token()) + .map_or(false, |name| { + matches!( + name.text_trimmed(), + "async" | "inject" | "fakeAsync" | "waitForAsync" + ) + }), + _ => false, + }, + _ => false, + } +} + +/// Tests if the callee is a `beforeEach`, `beforeAll`, `afterEach` or `afterAll` identifier +/// that is commonly used in test frameworks. +fn is_unit_test_set_up_callee(callee: &AnyJsExpression) -> bool { + match callee { + AnyJsExpression::JsIdentifierExpression(identifier) => identifier + .name() + .and_then(|name| name.value_token()) + .map_or(false, |name| { + matches!( + name.text_trimmed(), + "beforeEach" | "beforeAll" | "afterEach" | "afterAll" + ) + }), + _ => false, + } } impl JsNewExpression { @@ -1312,3 +1724,259 @@ impl TsStringLiteralType { Ok(inner_string_text(&self.literal_token()?)) } } + +#[cfg(test)] +mod test { + use biome_js_factory::syntax::{JsCallExpression, JsTemplateExpression}; + use biome_js_parser::parse_module; + use biome_js_parser::JsParserOptions; + use biome_rowan::AstNodeList; + + fn extract_call_expression(src: &str) -> JsCallExpression { + let result = parse_module(src, JsParserOptions::default()); + let module = result.tree().items().first().unwrap(); + + module + .as_any_js_statement() + .unwrap() + .as_js_expression_statement() + .unwrap() + .expression() + .unwrap() + .as_js_call_expression() + .unwrap() + .clone() + } + + fn extract_template(src: &str) -> JsTemplateExpression { + let result = parse_module(src, JsParserOptions::default()); + let module = result.tree().items().first().unwrap(); + + module + .as_any_js_statement() + .unwrap() + .as_js_expression_statement() + .unwrap() + .expression() + .unwrap() + .as_js_template_expression() + .unwrap() + .clone() + } + + #[test] + fn matches_simple_call() { + let call_expression = extract_call_expression("test();"); + assert_eq!( + call_expression.callee().unwrap().contains_a_test_pattern(), + Ok(true) + ); + + let call_expression = extract_call_expression("it();"); + assert_eq!( + call_expression.callee().unwrap().contains_a_test_pattern(), + Ok(true) + ); + } + + #[test] + fn matches_static_member_expression() { + let call_expression = extract_call_expression("test.only();"); + assert_eq!( + call_expression.callee().unwrap().contains_a_test_pattern(), + Ok(true) + ); + } + + #[test] + fn matches_static_member_expression_deep() { + let call_expression = extract_call_expression("test.describe.parallel.only();"); + assert_eq!( + call_expression.callee().unwrap().contains_a_test_pattern(), + Ok(true) + ); + } + + #[test] + fn doesnt_static_member_expression_deep() { + let call_expression = extract_call_expression("test.describe.parallel.only.AHAHA();"); + assert_eq!( + call_expression.callee().unwrap().contains_a_test_pattern(), + Ok(false) + ); + } + + #[test] + fn matches_simple_each() { + let template = extract_template("describe.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("test.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("it.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("xdescribe.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("xtest.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("xit.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("fdescribe.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("ftest.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("fit.each``"); + assert!(template.is_test_each_pattern_callee()); + } + + #[test] + fn matches_skip_each() { + let template = extract_template("describe.skip.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("test.skip.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("it.skip.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("xdescribe.skip.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("xtest.skip.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("xit.skip.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("fdescribe.skip.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("ftest.skip.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("fit.skip.each``"); + assert!(template.is_test_each_pattern_callee()); + } + + #[test] + fn matches_only_each() { + let template = extract_template("describe.only.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("test.only.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("it.only.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("xdescribe.only.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("xtest.only.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("xit.only.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("fdescribe.only.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("ftest.only.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("fit.only.each``"); + assert!(template.is_test_each_pattern_callee()); + } + + #[test] + fn matches_failing_each() { + let template = extract_template("test.failing.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("it.failing.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("xtest.failing.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("xit.failing.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("ftest.failing.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("fit.failing.each``"); + assert!(template.is_test_each_pattern_callee()); + } + + #[test] + fn matches_concurrent_each() { + let template = extract_template("test.concurrent.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("it.concurrent.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("xtest.concurrent.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("xit.concurrent.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("ftest.concurrent.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("fit.concurrent.each``"); + assert!(template.is_test_each_pattern_callee()); + } + + #[test] + fn matches_concurrent_only_each() { + let template = extract_template("test.concurrent.only.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("it.concurrent.only.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("xtest.concurrent.only.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("xit.concurrent.only.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("ftest.concurrent.only.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("fit.concurrent.only.each``"); + assert!(template.is_test_each_pattern_callee()); + } + + #[test] + fn matches_concurrent_skip_each() { + let template = extract_template("test.concurrent.skip.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("it.concurrent.skip.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("xtest.concurrent.skip.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("xit.concurrent.skip.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("ftest.concurrent.skip.each``"); + assert!(template.is_test_each_pattern_callee()); + + let template = extract_template("fit.concurrent.skip.each``"); + assert!(template.is_test_each_pattern_callee()); + } +} diff --git a/crates/biome_js_syntax/src/lib.rs b/crates/biome_js_syntax/src/lib.rs index 436e43cd4e31..b83c3322cbd3 100644 --- a/crates/biome_js_syntax/src/lib.rs +++ b/crates/biome_js_syntax/src/lib.rs @@ -39,7 +39,7 @@ pub use stmt_ext::*; pub use syntax_node::*; use crate::JsSyntaxKind::*; -use biome_rowan::{AstNode, RawSyntaxKind}; +use biome_rowan::{AstNode, RawSyntaxKind, SyntaxResult}; impl From for JsSyntaxKind { fn from(d: u16) -> JsSyntaxKind { @@ -308,3 +308,14 @@ pub fn inner_string_text(token: &JsSyntaxToken) -> TokenText { } text } + +/// Returns `Ok(true)` if `maybe_argument` is an argument of a [test call expression](is_test_call_expression). +pub fn is_test_call_argument(maybe_argument: &JsSyntaxNode) -> SyntaxResult { + let call_expression = maybe_argument + .parent() + .and_then(JsCallArgumentList::cast) + .and_then(|args| args.syntax().grand_parent()) + .and_then(JsCallExpression::cast); + + call_expression.map_or(Ok(false), |call| call.is_test_call_expression()) +} diff --git a/crates/biome_service/src/configuration/linter/rules.rs b/crates/biome_service/src/configuration/linter/rules.rs index ca51615f37de..624fc8184261 100644 --- a/crates/biome_service/src/configuration/linter/rules.rs +++ b/crates/biome_service/src/configuration/linter/rules.rs @@ -2254,6 +2254,9 @@ pub struct Nursery { #[doc = "Disallow empty type parameters in type aliases and interfaces."] #[serde(skip_serializing_if = "Option::is_none")] pub no_empty_type_parameters: Option, + #[doc = "Disallow focused tests."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_focused_tests: Option, #[doc = "Disallow assignments to native objects and read-only global variables."] #[serde(skip_serializing_if = "Option::is_none")] pub no_global_assign: Option, @@ -2269,6 +2272,9 @@ pub struct Nursery { #[doc = "Forbid the use of Node.js builtin modules."] #[serde(skip_serializing_if = "Option::is_none")] pub no_nodejs_modules: Option, + #[doc = "Disallow disabled tests."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_skipped_tests: Option, #[doc = "Disallow then property."] #[serde(skip_serializing_if = "Option::is_none")] pub no_then_property: Option, @@ -2334,15 +2340,17 @@ impl DeserializableValidator for Nursery { } impl Nursery { const GROUP_NAME: &'static str = "nursery"; - pub(crate) const GROUP_RULES: [&'static str; 24] = [ + pub(crate) const GROUP_RULES: [&'static str; 26] = [ "noDuplicateJsonKeys", "noEmptyBlockStatements", "noEmptyTypeParameters", + "noFocusedTests", "noGlobalAssign", "noGlobalEval", "noInvalidUseBeforeDeclaration", "noMisleadingCharacterClass", "noNodejsModules", + "noSkippedTests", "noThenProperty", "noUnusedImports", "noUnusedPrivateClassMembers", @@ -2360,9 +2368,10 @@ impl Nursery { "useNumberNamespace", "useShorthandFunctionType", ]; - const RECOMMENDED_RULES: [&'static str; 11] = [ + const RECOMMENDED_RULES: [&'static str; 12] = [ "noDuplicateJsonKeys", "noEmptyTypeParameters", + "noFocusedTests", "noGlobalAssign", "noGlobalEval", "noThenProperty", @@ -2373,20 +2382,21 @@ impl Nursery { "useImportType", "useNumberNamespace", ]; - const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 11] = [ + const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 12] = [ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]), ]; - const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 24] = [ + const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 26] = [ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), @@ -2411,6 +2421,8 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended(&self) -> bool { @@ -2442,111 +2454,121 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2])); } } - if let Some(rule) = self.no_global_assign.as_ref() { + if let Some(rule) = self.no_focused_tests.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3])); } } - if let Some(rule) = self.no_global_eval.as_ref() { + if let Some(rule) = self.no_global_assign.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4])); } } - if let Some(rule) = self.no_invalid_use_before_declaration.as_ref() { + if let Some(rule) = self.no_global_eval.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5])); } } - if let Some(rule) = self.no_misleading_character_class.as_ref() { + if let Some(rule) = self.no_invalid_use_before_declaration.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6])); } } - if let Some(rule) = self.no_nodejs_modules.as_ref() { + if let Some(rule) = self.no_misleading_character_class.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); } } - if let Some(rule) = self.no_then_property.as_ref() { + if let Some(rule) = self.no_nodejs_modules.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } } - if let Some(rule) = self.no_unused_imports.as_ref() { + if let Some(rule) = self.no_skipped_tests.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } } - if let Some(rule) = self.no_unused_private_class_members.as_ref() { + if let Some(rule) = self.no_then_property.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } } - if let Some(rule) = self.no_useless_lone_block_statements.as_ref() { + if let Some(rule) = self.no_unused_imports.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - if let Some(rule) = self.no_useless_ternary.as_ref() { + if let Some(rule) = self.no_unused_private_class_members.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } } - if let Some(rule) = self.use_await.as_ref() { + if let Some(rule) = self.no_useless_lone_block_statements.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } } - if let Some(rule) = self.use_consistent_array_type.as_ref() { + if let Some(rule) = self.no_useless_ternary.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } } - if let Some(rule) = self.use_export_type.as_ref() { + if let Some(rule) = self.use_await.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } } - if let Some(rule) = self.use_filenaming_convention.as_ref() { + if let Some(rule) = self.use_consistent_array_type.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.use_for_of.as_ref() { + if let Some(rule) = self.use_export_type.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.use_grouped_type_import.as_ref() { + if let Some(rule) = self.use_filenaming_convention.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_for_of.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.use_import_type.as_ref() { + if let Some(rule) = self.use_grouped_type_import.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.use_nodejs_import_protocol.as_ref() { + if let Some(rule) = self.use_import_restrictions.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.use_number_namespace.as_ref() { + if let Some(rule) = self.use_import_type.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.use_shorthand_function_type.as_ref() { + if let Some(rule) = self.use_nodejs_import_protocol.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } + if let Some(rule) = self.use_number_namespace.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); + } + } + if let Some(rule) = self.use_shorthand_function_type.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> IndexSet { @@ -2566,111 +2588,121 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2])); } } - if let Some(rule) = self.no_global_assign.as_ref() { + if let Some(rule) = self.no_focused_tests.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3])); } } - if let Some(rule) = self.no_global_eval.as_ref() { + if let Some(rule) = self.no_global_assign.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4])); } } - if let Some(rule) = self.no_invalid_use_before_declaration.as_ref() { + if let Some(rule) = self.no_global_eval.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5])); } } - if let Some(rule) = self.no_misleading_character_class.as_ref() { + if let Some(rule) = self.no_invalid_use_before_declaration.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6])); } } - if let Some(rule) = self.no_nodejs_modules.as_ref() { + if let Some(rule) = self.no_misleading_character_class.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); } } - if let Some(rule) = self.no_then_property.as_ref() { + if let Some(rule) = self.no_nodejs_modules.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } } - if let Some(rule) = self.no_unused_imports.as_ref() { + if let Some(rule) = self.no_skipped_tests.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } } - if let Some(rule) = self.no_unused_private_class_members.as_ref() { + if let Some(rule) = self.no_then_property.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } } - if let Some(rule) = self.no_useless_lone_block_statements.as_ref() { + if let Some(rule) = self.no_unused_imports.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - if let Some(rule) = self.no_useless_ternary.as_ref() { + if let Some(rule) = self.no_unused_private_class_members.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } } - if let Some(rule) = self.use_await.as_ref() { + if let Some(rule) = self.no_useless_lone_block_statements.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } } - if let Some(rule) = self.use_consistent_array_type.as_ref() { + if let Some(rule) = self.no_useless_ternary.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } } - if let Some(rule) = self.use_export_type.as_ref() { + if let Some(rule) = self.use_await.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } } - if let Some(rule) = self.use_filenaming_convention.as_ref() { + if let Some(rule) = self.use_consistent_array_type.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.use_for_of.as_ref() { + if let Some(rule) = self.use_export_type.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.use_grouped_type_import.as_ref() { + if let Some(rule) = self.use_filenaming_convention.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_for_of.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.use_import_type.as_ref() { + if let Some(rule) = self.use_grouped_type_import.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.use_nodejs_import_protocol.as_ref() { + if let Some(rule) = self.use_import_restrictions.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.use_number_namespace.as_ref() { + if let Some(rule) = self.use_import_type.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.use_shorthand_function_type.as_ref() { + if let Some(rule) = self.use_nodejs_import_protocol.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } + if let Some(rule) = self.use_number_namespace.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); + } + } + if let Some(rule) = self.use_shorthand_function_type.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -2681,10 +2713,10 @@ impl Nursery { pub(crate) fn is_recommended_rule(rule_name: &str) -> bool { Self::RECOMMENDED_RULES.contains(&rule_name) } - pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 11] { + pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 12] { Self::RECOMMENDED_RULES_AS_FILTERS } - pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 24] { + pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 26] { Self::ALL_RULES_AS_FILTERS } #[doc = r" Select preset rules"] @@ -2710,11 +2742,13 @@ impl Nursery { "noDuplicateJsonKeys" => self.no_duplicate_json_keys.as_ref(), "noEmptyBlockStatements" => self.no_empty_block_statements.as_ref(), "noEmptyTypeParameters" => self.no_empty_type_parameters.as_ref(), + "noFocusedTests" => self.no_focused_tests.as_ref(), "noGlobalAssign" => self.no_global_assign.as_ref(), "noGlobalEval" => self.no_global_eval.as_ref(), "noInvalidUseBeforeDeclaration" => self.no_invalid_use_before_declaration.as_ref(), "noMisleadingCharacterClass" => self.no_misleading_character_class.as_ref(), "noNodejsModules" => self.no_nodejs_modules.as_ref(), + "noSkippedTests" => self.no_skipped_tests.as_ref(), "noThenProperty" => self.no_then_property.as_ref(), "noUnusedImports" => self.no_unused_imports.as_ref(), "noUnusedPrivateClassMembers" => self.no_unused_private_class_members.as_ref(), diff --git a/crates/biome_service/tests/invalid/hooks_missing_name.json.snap b/crates/biome_service/tests/invalid/hooks_missing_name.json.snap index 87d24f439533..2f8718c1251d 100644 --- a/crates/biome_service/tests/invalid/hooks_missing_name.json.snap +++ b/crates/biome_service/tests/invalid/hooks_missing_name.json.snap @@ -20,11 +20,13 @@ hooks_missing_name.json:6:5 deserialize ━━━━━━━━━━━━━ - noDuplicateJsonKeys - noEmptyBlockStatements - noEmptyTypeParameters + - noFocusedTests - noGlobalAssign - noGlobalEval - noInvalidUseBeforeDeclaration - noMisleadingCharacterClass - noNodejsModules + - noSkippedTests - noThenProperty - noUnusedImports - noUnusedPrivateClassMembers diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 863209ae5f1e..45f3ff3d1233 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -866,6 +866,10 @@ export interface Nursery { * Disallow empty type parameters in type aliases and interfaces. */ noEmptyTypeParameters?: RuleConfiguration; + /** + * Disallow focused tests. + */ + noFocusedTests?: RuleConfiguration; /** * Disallow assignments to native objects and read-only global variables. */ @@ -886,6 +890,10 @@ export interface Nursery { * Forbid the use of Node.js builtin modules. */ noNodejsModules?: RuleConfiguration; + /** + * Disallow disabled tests. + */ + noSkippedTests?: RuleConfiguration; /** * Disallow then property. */ @@ -1667,11 +1675,13 @@ export type Category = | "lint/nursery/noDuplicateJsonKeys" | "lint/nursery/noEmptyBlockStatements" | "lint/nursery/noEmptyTypeParameters" + | "lint/nursery/noFocusedTests" | "lint/nursery/noGlobalAssign" | "lint/nursery/noGlobalEval" | "lint/nursery/noInvalidUseBeforeDeclaration" | "lint/nursery/noMisleadingCharacterClass" | "lint/nursery/noNodejsModules" + | "lint/nursery/noSkippedTests" | "lint/nursery/noThenProperty" | "lint/nursery/noTypeOnlyImportAttributes" | "lint/nursery/noUnusedImports" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 1582d2d82a4f..db808fd3876d 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1303,6 +1303,13 @@ { "type": "null" } ] }, + "noFocusedTests": { + "description": "Disallow focused tests.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noGlobalAssign": { "description": "Disallow assignments to native objects and read-only global variables.", "anyOf": [ @@ -1338,6 +1345,13 @@ { "type": "null" } ] }, + "noSkippedTests": { + "description": "Disallow disabled tests.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noThenProperty": { "description": "Disallow then property.", "anyOf": [ diff --git a/website/src/components/generated/NumberOfRules.astro b/website/src/components/generated/NumberOfRules.astro index bcdbd5a4928e..87dc1678b0ec 100644 --- a/website/src/components/generated/NumberOfRules.astro +++ b/website/src/components/generated/NumberOfRules.astro @@ -1,2 +1,2 @@ -

Biome's linter has a total of 194 rules

\ No newline at end of file +

Biome's linter has a total of 196 rules

\ No newline at end of file diff --git a/website/src/content/docs/internals/changelog.mdx b/website/src/content/docs/internals/changelog.mdx index 101814f15a65..639f0efa5cc6 100644 --- a/website/src/content/docs/internals/changelog.mdx +++ b/website/src/content/docs/internals/changelog.mdx @@ -40,6 +40,25 @@ Read our [guidelines for writing a good changelog entry](https://github.com/biom ### Linter +#### New features + +- Add the rule [noSkippedTests](https://biomejs.dev/linter/rules/no-skipped-tests), to disallow skipped tests: + + ```js + describe.skip("test", () => {}); + it.skip("test", () => {}); + ``` +<<<<<<< HEAD +======= + +- Add the rule [noFocusedTests](https://biomejs.dev/linter/rules/no-focused-tests), to disallow skipped tests: + + ```js + describe.only("test", () => {}); + it.only("test", () => {}); + ``` + +>>>>>>> fd3de977d1 (feat(linter): new rule noFocusedTests (#1641)) ### Parser ## 1.5.3 (2024-01-22) diff --git a/website/src/content/docs/linter/rules/index.mdx b/website/src/content/docs/linter/rules/index.mdx index 4f95ec37e41a..14c2ac281c6f 100644 --- a/website/src/content/docs/linter/rules/index.mdx +++ b/website/src/content/docs/linter/rules/index.mdx @@ -235,11 +235,13 @@ Rules that belong to this group are not subject to semantic versioneval(). | | | [noInvalidUseBeforeDeclaration](/linter/rules/no-invalid-use-before-declaration) | Disallow the use of variables and function parameters before their declaration | | | [noMisleadingCharacterClass](/linter/rules/no-misleading-character-class) | Disallow characters made with multiple code points in character class syntax. | 🔧 | | [noNodejsModules](/linter/rules/no-nodejs-modules) | Forbid the use of Node.js builtin modules. | | +| [noSkippedTests](/linter/rules/no-skipped-tests) | Disallow disabled tests. | | | [noThenProperty](/linter/rules/no-then-property) | Disallow then property. | | | [noUnusedImports](/linter/rules/no-unused-imports) | Disallow unused imports. | 🔧 | | [noUnusedPrivateClassMembers](/linter/rules/no-unused-private-class-members) | Disallow unused private class members | ⚠️ | diff --git a/website/src/content/docs/linter/rules/no-focused-tests.md b/website/src/content/docs/linter/rules/no-focused-tests.md new file mode 100644 index 000000000000..3c86c6c97e5c --- /dev/null +++ b/website/src/content/docs/linter/rules/no-focused-tests.md @@ -0,0 +1,66 @@ +--- +title: noFocusedTests (not released) +--- + +**Diagnostic Category: `lint/nursery/noFocusedTests`** + +:::danger +This rule hasn't been released yet. +::: + +:::caution +This rule is part of the [nursery](/linter/rules/#nursery) group. +::: + +Inspired from: no-focused-tests + +Disallow focused tests. + +Disabled test are useful when developing and debugging, because it forces the test suite to run only certain tests. + +However, in pull/merge request, you usually want to run all the test suite. + +## Examples + +### Invalid + +```jsx +describe.only("foo", () => {}); +``` + +

nursery/noFocusedTests.js:1:10 lint/nursery/noFocusedTests ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   Don't focus the test.
+  
+  > 1 │ describe.only("foo", () => {});
+            ^^^^
+    2 │ 
+  
+   This is likely a change done during debugging or implementation phases, but it's unlikely what you want in production.
+  
+   Remove it.
+  
+
+ +```jsx +test.only("foo", () => {}); +``` + +
nursery/noFocusedTests.js:1:6 lint/nursery/noFocusedTests ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   Don't focus the test.
+  
+  > 1 │ test.only("foo", () => {});
+        ^^^^
+    2 │ 
+  
+   This is likely a change done during debugging or implementation phases, but it's unlikely what you want in production.
+  
+   Remove it.
+  
+
+ +## Related links + +- [Disable a rule](/linter/#disable-a-lint-rule) +- [Rule options](/linter/#rule-options) diff --git a/website/src/content/docs/linter/rules/no-skipped-tests.md b/website/src/content/docs/linter/rules/no-skipped-tests.md new file mode 100644 index 000000000000..4843f117e70a --- /dev/null +++ b/website/src/content/docs/linter/rules/no-skipped-tests.md @@ -0,0 +1,71 @@ +--- +title: noSkippedTests (not released) +--- + +**Diagnostic Category: `lint/nursery/noSkippedTests`** + +:::danger +This rule hasn't been released yet. +::: + +:::caution +This rule is part of the [nursery](/linter/rules/#nursery) group. +::: + +Inspired from: no-disabled-tests + +Disallow disabled tests. + +Disabled test are useful when developing and debugging, although they should not be committed in production. + +## Examples + +### Invalid + +```jsx +describe.skip("test", () => {}); +``` + +
nursery/noSkippedTests.js:1:10 lint/nursery/noSkippedTests ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   Don't disable tests.
+  
+  > 1 │ describe.skip("test", () => {});
+            ^^^^
+    2 │ 
+  
+   Disabling tests is useful when debugging or creating placeholder while working.
+  
+   If this is intentional, and you want to commit a disabled test, add a suppression comment.
+  
+
+ +```jsx +test.skip("test", () => {}); +``` + +
nursery/noSkippedTests.js:1:6 lint/nursery/noSkippedTests ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   Don't disable tests.
+  
+  > 1 │ test.skip("test", () => {});
+        ^^^^
+    2 │ 
+  
+   Disabling tests is useful when debugging or creating placeholder while working.
+  
+   If this is intentional, and you want to commit a disabled test, add a suppression comment.
+  
+
+ +## Valid + +```jsx +test.only("test", () => {}); +test("test", () => {}); +``` + +## Related links + +- [Disable a rule](/linter/#disable-a-lint-rule) +- [Rule options](/linter/#rule-options) diff --git a/xtask/codegen/src/generate_new_lintrule.rs b/xtask/codegen/src/generate_new_lintrule.rs index ac826eb912f8..c0a2efa41ec2 100644 --- a/xtask/codegen/src/generate_new_lintrule.rs +++ b/xtask/codegen/src/generate_new_lintrule.rs @@ -36,8 +36,6 @@ declare_rule! {{ /// /// Add a link to the corresponding ESLint rule (if any): /// - /// Source: https://eslint.org/docs/latest/rules/rule-name - /// /// ## Examples /// /// ### Invalid