Skip to content

fix(noDuplicateTestHook): detect more test function defintions#7287

Merged
ematipico merged 15 commits intobiomejs:mainfrom
ToBinio:improve-no-duplicate-test-hook
Nov 11, 2025
Merged

fix(noDuplicateTestHook): detect more test function defintions#7287
ematipico merged 15 commits intobiomejs:mainfrom
ToBinio:improve-no-duplicate-test-hook

Conversation

@ToBinio
Copy link
Contributor

@ToBinio ToBinio commented Aug 21, 2025

Todo

  • changeset
  • improve diagnostic of noDuplicateTestHook

Summary

fixes #7205

This PR increases the amount of test functions being detected, fixing #7205 and improving anything else using these functions

Test Plan

updated tests


Note

please also let me know if the test_declarations.js.snap are valid

@changeset-bot
Copy link

changeset-bot bot commented Aug 21, 2025

🦋 Changeset detected

Latest commit: 7a25621

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@biomejs/biome Patch
@biomejs/cli-win32-x64 Patch
@biomejs/cli-win32-arm64 Patch
@biomejs/cli-darwin-x64 Patch
@biomejs/cli-darwin-arm64 Patch
@biomejs/cli-linux-x64 Patch
@biomejs/cli-linux-arm64 Patch
@biomejs/cli-linux-x64-musl Patch
@biomejs/cli-linux-arm64-musl Patch
@biomejs/wasm-web Patch
@biomejs/wasm-bundler Patch
@biomejs/wasm-nodejs Patch
@biomejs/backend-jsonrpc Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 21, 2025

Walkthrough

The PR centralises detection of "describe" call forms for the duplicate-test-hooks linter by adding an internal helper that recognises describe and chained describe variants (e.g., describe.skip, describe.each, describe.for, fdescribe, xdescribe) and uses it to push/pop hook scopes. The syntax crate API was changed: AnyJsExpression::contains_a_test_pattern now returns bool and a new contains_a_test_each_pattern helper was added. Tests for noDuplicateTestHooks were extended and no_skipped_tests updated to the new boolean API. A patch-level changeset was added.

Assessment against linked issues

Objective Addressed Explanation
Recognise Vitest chained describe variants (describe.XYZ, including .each/.for/.skip/.todo etc.) as describe blocks that create their own hook scope (#7205)
Centralise/adjust rule logic to treat member expressions where base is "describe" as describe, including chained forms (#7205)
Ensure only the first duplicate hook per describe scope is reported; no further erroneous diagnostics within that scope (#7205) Tests added appear to be valid-case scaffolding; no explicit failing-case assertions shown to confirm single-error reporting.
Include tests covering multiple Vitest describe variants to prevent regressions (#7205)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Change return type of AnyJsExpression::contains_a_test_pattern from SyntaxResult to bool and add contains_a_test_each_pattern (crates/biome_js_syntax/src/expr_ext.rs) Public API signature changed beyond the single-rule fix; this affects all callers and error-handling semantics not required by the issue.
Update no_skipped_tests to call contains_a_test_pattern as a bool (crates/biome_js_analyze/src/lint/suspicious/no_skipped_tests.rs) Call-site adaptation to the API change; touches a different lint rule and is not strictly required by #7205.

Suggested labels

A-Core

Suggested reviewers

  • dyc3
  • siketyan

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

‼️ IMPORTANT
Auto-reply has been disabled for this repository in the CodeRabbit settings. The CodeRabbit bot will not respond to your replies unless it is explicitly tagged.

  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot added A-Linter Area: linter A-Parser Area: parser A-Formatter Area: formatter L-JavaScript Language: JavaScript and super languages labels Aug 21, 2025
@codspeed-hq
Copy link

codspeed-hq bot commented Aug 22, 2025

CodSpeed Performance Report

Merging #7287 will not alter performance

Comparing ToBinio:improve-no-duplicate-test-hook (7a25621) with main (e403868)1

Summary

✅ 58 untouched
⏩ 95 skipped2

Footnotes

  1. No successful run was found on main (8b93578) during the generation of this report, so e403868 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

  2. 95 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@ToBinio ToBinio marked this pull request as ready for review August 22, 2025 16:05
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
crates/biome_js_syntax/src/expr_ext.rs (3)

932-1010: Broadened test pattern matcher looks solid; consider a tiny readability pass.

The finite-state style with CalleeNamesIterator is sound and covers the extended sets (describe.{shuffle|sequential|…}, it/test.{failing|…}, test.describe.*). For maintainability, consider hoisting the allowed-token sets into small const arrays and using matches!(...) || ARR.contains(...) to reduce repetition.


1999-2040: Guard with clearer control flow (minor ergonomics).

The matches! with a ? in the guard is clever but a tad terse. An explicit if let reads easier and avoids early-? surprises.

-                if arguments.args().len() <= 3
-                    && (callee.contains_a_test_pattern()
-                        || matches!(callee, AnyJsExpression::JsCallExpression(call_expression) if call_expression.callee()?.contains_a_test_each_pattern())) =>
+                if arguments.args().len() <= 3
+                    && (callee.contains_a_test_pattern()
+                        || {
+                            if let AnyJsExpression::JsCallExpression(inner) = callee {
+                                inner.callee()?.contains_a_test_each_pattern()
+                            } else {
+                                false
+                            }
+                        }) =>

2208-2212: Nice test coverage; add a couple of edge cases?

Great to see deep chains and .each calls exercised. Two quick additions would harden confidence:

  • describe.for([])("name", () => {}) via a call-expression callee (already covered as template-less array; good).
  • Computed access: describe["each"]([])("name", () => {}) and optional: describe?.each?.([])("name", () => {}) (after implementing computed/optional support).

Also applies to: 2217-2228, 2258-2270

crates/biome_js_analyze/tests/specs/suspicious/noDuplicateTestHooks/valid.js (1)

85-106: Good: validates chained describe variants as separate scopes.

This neatly covers describe.skip.each, describe.for, describe.todo, and describe.todo.each. Consider adding:

  • describe.sequential(...) and describe.concurrent(...) with hooks.
  • A test.describe.parallel("name", () => { beforeEach(...); }) case if we choose to support Playwright-style test.describe in the rule.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1511d0c and fba6c5d.

⛔ Files ignored due to path filters (3)
  • crates/biome_js_analyze/tests/specs/suspicious/noDuplicateTestHooks/invalid.js.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/suspicious/noDuplicateTestHooks/valid.js.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_formatter/tests/specs/prettier/js/test-declarations/test_declarations.js.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (5)
  • .changeset/puny-turtles-sniff.md (1 hunks)
  • crates/biome_js_analyze/src/lint/suspicious/no_duplicate_test_hooks.rs (4 hunks)
  • crates/biome_js_analyze/src/lint/suspicious/no_skipped_tests.rs (1 hunks)
  • crates/biome_js_analyze/tests/specs/suspicious/noDuplicateTestHooks/valid.js (1 hunks)
  • crates/biome_js_syntax/src/expr_ext.rs (7 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
.changeset/*.md

📄 CodeRabbit inference engine (CONTRIBUTING.md)

.changeset/*.md: Create changesets with just new-changeset; store them in .changeset/ with correct frontmatter (package keys and change type).
In changeset descriptions, follow content conventions: user-facing changes only; past tense for what you did; present tense for current behavior; link issues for fixes; link rules/assists; include representative code blocks; end every sentence with a period.
When adding headers in a changeset, only use #### or ##### levels.

Files:

  • .changeset/puny-turtles-sniff.md
crates/biome_*_{syntax,parser,formatter,analyze,factory,semantic}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Maintain the per-language crate structure: biome_{lang}_{syntax,parser,formatter,analyze,factory,semantic}

Files:

  • crates/biome_js_analyze/tests/specs/suspicious/noDuplicateTestHooks/valid.js
  • crates/biome_js_analyze/src/lint/suspicious/no_duplicate_test_hooks.rs
  • crates/biome_js_analyze/src/lint/suspicious/no_skipped_tests.rs
  • crates/biome_js_syntax/src/expr_ext.rs
crates/biome_*/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place core crates under /crates/biome_*/

Files:

  • crates/biome_js_analyze/tests/specs/suspicious/noDuplicateTestHooks/valid.js
  • crates/biome_js_analyze/src/lint/suspicious/no_duplicate_test_hooks.rs
  • crates/biome_js_analyze/src/lint/suspicious/no_skipped_tests.rs
  • crates/biome_js_syntax/src/expr_ext.rs
**/tests/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place test files under a tests/ directory in each crate

Files:

  • crates/biome_js_analyze/tests/specs/suspicious/noDuplicateTestHooks/valid.js
**/*.{rs,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Format Rust and TOML files before committing (use just f/just format).

Files:

  • crates/biome_js_analyze/src/lint/suspicious/no_duplicate_test_hooks.rs
  • crates/biome_js_analyze/src/lint/suspicious/no_skipped_tests.rs
  • crates/biome_js_syntax/src/expr_ext.rs
🧠 Learnings (1)
📚 Learning: 2025-08-17T08:56:30.831Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-08-17T08:56:30.831Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/tests/quick_test.rs : Use `biome_js_analyze/tests/quick_test.rs` for quick, ad-hoc testing; un-ignore the test and adjust the rule filter as needed

Applied to files:

  • crates/biome_js_analyze/src/lint/suspicious/no_skipped_tests.rs
🧬 Code graph analysis (2)
crates/biome_js_analyze/src/lint/suspicious/no_duplicate_test_hooks.rs (1)
crates/biome_js_syntax/src/expr_ext.rs (2)
  • callee (32-37)
  • text (1780-1785)
crates/biome_js_analyze/src/lint/suspicious/no_skipped_tests.rs (1)
crates/biome_js_syntax/src/expr_ext.rs (1)
  • callee (32-37)
🔇 Additional comments (4)
crates/biome_js_syntax/src/expr_ext.rs (1)

693-699: Good call: delegate each-pattern detection to the callee.

Using tag.contains_a_test_each_pattern() removes duplicated logic and keeps “each/for” recognition in one place. LGTM.

crates/biome_js_analyze/src/lint/suspicious/no_skipped_tests.rs (1)

65-65: All clear—no more .ok()? calls lurking!

Great job migrating the API; I spot no stragglers. 🎉

crates/biome_js_analyze/src/lint/suspicious/no_duplicate_test_hooks.rs (2)

124-126: Scope entry/exit on describe-like calls is the right move.

Centralising the scope gate via is_test_describe_call simplifies the visitor and avoids per-variant checks. Nice.

Also applies to: 153-156


229-234: Dynamic hook name in the diagnostic is a usability win.

“Duplicate "" hook found.” is clearer for users. Nicely done.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
crates/biome_js_analyze/tests/specs/suspicious/noDuplicateTestHooks/valid.js (4)

92-107: Add describe.sequential and order-swapped chaining (each(...).skip)

Issue #7205 calls out Vitest’s describe.sequential. Also worth asserting that chaining works in both orders (e.g., describe.each(...).skip(...)) to future-proof the selector logic.

Apply this minimal addition near this block:

+describe.sequential("something", () => {
+  beforeEach(() => {});
+});
+
+describe.each([]).skip("something", () => {
+  beforeEach(() => {});
+});

116-119: Broaden Playwright-style coverage with base and serial groups

You’ve got test.describe.skip.each. Consider also asserting plain test.describe and test.describe.serial create their own hook scopes.

Apply this small addition:

+test.describe("something", () => {
+  beforeEach(() => {});
+});
+
+test.describe.serial("something", () => {
+  beforeEach(() => {});
+});

120-126: Round out aliases with .each on xdescribe/fdescribe

If we treat these aliases as describes, it’s useful to show they also work with each(...).

Add:

+xdescribe.each([])("something", () => {
+  beforeEach(() => {});
+});
+
+fdescribe.each([])("something", () => {
+  beforeEach(() => {});
+});

92-107: Exercise other hook kinds inside a chained describe

To demonstrate hook-type agnosticism, include at least one afterEach/beforeAll/afterAll inside a chained describe variant.

For example, expand the .todo.each block:

   describe.todo.each([])("something", () => {
     beforeEach(() => {});
+    afterEach(() => {});
+    beforeAll(() => {});
+    afterAll(() => {});
   });
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fba6c5d and bfdd7a4.

⛔ Files ignored due to path filters (1)
  • crates/biome_js_analyze/tests/specs/suspicious/noDuplicateTestHooks/valid.js.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (3)
  • .changeset/puny-turtles-sniff.md (1 hunks)
  • crates/biome_js_analyze/src/lint/suspicious/no_duplicate_test_hooks.rs (4 hunks)
  • crates/biome_js_analyze/tests/specs/suspicious/noDuplicateTestHooks/valid.js (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .changeset/puny-turtles-sniff.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/biome_js_analyze/src/lint/suspicious/no_duplicate_test_hooks.rs
🧰 Additional context used
📓 Path-based instructions (3)
crates/biome_*_{syntax,parser,formatter,analyze,factory,semantic}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Maintain the per-language crate structure: biome_{lang}_{syntax,parser,formatter,analyze,factory,semantic}

Files:

  • crates/biome_js_analyze/tests/specs/suspicious/noDuplicateTestHooks/valid.js
crates/biome_*/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place core crates under /crates/biome_*/

Files:

  • crates/biome_js_analyze/tests/specs/suspicious/noDuplicateTestHooks/valid.js
**/tests/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place test files under a tests/ directory in each crate

Files:

  • crates/biome_js_analyze/tests/specs/suspicious/noDuplicateTestHooks/valid.js
🔇 Additional comments (1)
crates/biome_js_analyze/tests/specs/suspicious/noDuplicateTestHooks/valid.js (1)

85-127: Good expansion of chained describe coverage

Covers describe.skip.each, describe.for, describe.todo, describe.todo.each, test.describe.skip.each, plus xdescribe/fdescribe. This aligns well with the goal of recognising chained describe variants. Nice one.

@github-actions github-actions bot added the A-CLI Area: CLI label Aug 23, 2025
}

impl DuplicateHooksVisitor {
fn is_test_describe_call(node: &JsCallExpression) -> bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a small doc comment that explains the business logic of the function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried, is that good enough?

/// - `test.describe.serial`
/// - `test.describe.serial.only`
///
/// - `it[.(only|skip|todo|fails|failing|concurrent|sequential)]*`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I preferred the list because it was plain and simple. We changed to some sort of regular expression, and I don't even understand what the * or ? even mean. Was the list so difficult to maintain?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I did the math correctly, it would be around 150 cases,
if you want I can add it, but it feels a bit much

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I suggest rewriting the docs in a way that is more readable, or maybe explain the syntax you proposed. I can't understand it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried simplified them, let me know if you feel like they are understandable now


let mut rev = texts.iter().rev().flatten();
pub fn contains_a_test_pattern(&self) -> bool {
let members = CalleeNamesIterator::new(self.clone()).collect::<Vec<_>>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this change. Why do we collect the iterator? I think the previous code was fine.

Plus, by collecting, we don't follow the article attached to the function anymore....

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous code fails if there are more than 5 segments in the call

e.g

test.skip.sequential.only.skip.sequential.only();
				^
				this would be "first"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand, thank you. Then the documentation of the function must be updated to reflect the new logic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the docs to note that it only uses the tries data structure from that article

}

/// A valid pattern:
/// - Starts with a valid test pattern
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's a "valid test pattern"? You might want to expand it, or link the previous function.

Also, can provide some links to the original test frameworks that use this patterns?

Also, let's provide examples. Let's follow the same high standard of the other function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried, is that good enough?

/// 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we remove the doc comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this method no longer handles the logic, I moved it to the function that does - contains_a_test_each_pattern

let me know if I should revert that

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then the documentation must be updated, not removed :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a small doc block linking to the function containing the logic

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/biome_js_analyze/src/lint/suspicious/no_duplicate_test_hooks.rs (1)

135-142: Emit only the first duplicate within a scope

Right now, counter > 1 will flag the 2nd, 3rd, 4th… occurrences. The issue’s requirement is to report only the first duplicate per describe scope. Gate on == 2 instead.

Apply:

-                                        if counter > 1 {
+                                        if counter == 2 {
                                             ctx.match_query(DuplicateHooks(node.clone()));
                                         }
🧹 Nitpick comments (1)
crates/biome_js_analyze/src/lint/suspicious/no_duplicate_test_hooks.rs (1)

162-191: Broaden describe detection to any .describe segment (keeps Playwright and future chains covered)

The current split-based check handles describe.*, fdescribe, xdescribe, and test.describe.* (second segment). To be more future-proof (e.g. deeper chains), consider accepting any segment equal to describe once is_test_call_expression() has gated it as a test call. This also simplifies the logic.

Apply:

 impl DuplicateHooksVisitor {
@@
-    /// 2. The test call must be a `describe.` call
-    ///     first section = `describe` | `fdescribe` | `xdescribe` e.g. `describe.only`, `describe.skip`
-    ///     or
-    ///     second section = `describe` e.g. `test.describe`
+    /// 2. The test call’s callee chain contains a `describe` segment
+    ///    (handles `describe.*`, `fdescribe`/`xdescribe`, and `test.describe.*`)
@@
-    fn is_test_describe_call(node: &JsCallExpression) -> bool {
-        if let Ok(callee) = node.callee()
-            && node.is_test_call_expression() == Ok(true)
-        {
-            let text = callee.to_trimmed_text();
-            let mut split = text.split('.');
-
-            let first = split.next();
-            let second = split.next();
-
-            if matches!(first, Some("describe" | "fdescribe" | "xdescribe"))
-                || second == Some("describe")
-            {
-                return true;
-            }
-        }
-
-        false
-    }
+    fn is_test_describe_call(node: &JsCallExpression) -> bool {
+        if node.is_test_call_expression() != Ok(true) {
+            return false;
+        }
+        if let Ok(callee) = node.callee() {
+            let chain = callee.to_trimmed_text();
+            // Accept any chain that contains a `describe` segment
+            // (e.g. describe.only, describe.each, test.describe, test.describe.parallel, etc.)
+            return chain.split('.').any(|seg| {
+                matches!(seg, "describe" | "fdescribe" | "xdescribe")
+            });
+        }
+        false
+    }

Small nit: the doc link says [call expression]: crate::JsCallExpression; consider biome_js_syntax::JsCallExpression for clarity.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bfdd7a4 and 310b05a.

⛔ Files ignored due to path filters (3)
  • crates/biome_cli/tests/snapshots/main_cases_linter_domains/does_enable_test_rules.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_linter_domains/does_enable_test_rules_when_recommended_rules_are_disabled_but_domain_is_enabled.snap is excluded by !**/*.snap and included by **
  • crates/biome_cli/tests/snapshots/main_cases_rules_via_dependencies/enables_test_globals_via_dependencies.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (3)
  • .changeset/puny-turtles-sniff.md (1 hunks)
  • crates/biome_js_analyze/src/lint/suspicious/no_duplicate_test_hooks.rs (4 hunks)
  • crates/biome_js_syntax/src/expr_ext.rs (7 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .changeset/puny-turtles-sniff.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/biome_js_syntax/src/expr_ext.rs
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{rs,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Format Rust and TOML files before committing (use just f/just format).

Files:

  • crates/biome_js_analyze/src/lint/suspicious/no_duplicate_test_hooks.rs
crates/biome_*_{syntax,parser,formatter,analyze,factory,semantic}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Maintain the per-language crate structure: biome_{lang}_{syntax,parser,formatter,analyze,factory,semantic}

Files:

  • crates/biome_js_analyze/src/lint/suspicious/no_duplicate_test_hooks.rs
crates/biome_*/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place core crates under /crates/biome_*/

Files:

  • crates/biome_js_analyze/src/lint/suspicious/no_duplicate_test_hooks.rs
🧬 Code graph analysis (1)
crates/biome_js_analyze/src/lint/suspicious/no_duplicate_test_hooks.rs (1)
crates/biome_js_syntax/src/expr_ext.rs (1)
  • callee (32-37)
🔇 Additional comments (3)
crates/biome_js_analyze/src/lint/suspicious/no_duplicate_test_hooks.rs (3)

124-126: Centralised scope entry looks good

Nice consolidation via is_test_describe_call; this keeps push logic in one place.


153-156: Symmetric scope exit

Pop mirrors the entry condition, so stack discipline stays correct.


243-248: Clearer diagnostics

Dynamic hook name in the primary message plus a concise note reads well and is actionable.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/biome_js_syntax/src/expr_ext.rs (1)

936-961: Docs out of sync with implementation (prefix match vs “tries”)

The comment still references “tries”/state machines, but the code now does a prefix match over collected identifiers. Also worth stating explicitly that trailing qualifiers beyond the documented patterns are allowed (that’s why very long chains pass now).

Proposed doc tweak:

 /// This function checks if a call expressions has one of the following members:
@@
-/// Elements within parentheses `()` can be any of the listed options separated by `|`.
-///
-/// Implementation first collects the tokens of callee names
-/// and checks if they match any of the listed options via the tries data structure described in this [article].
-///
-/// [article]: https://craftinginterpreters.com/scanning-on-demand.html#tries-and-state-machines
+/// Parenthesised groups denote alternatives. Matching is prefix-based:
+/// additional chained qualifiers beyond those shown are allowed and ignored
+/// (e.g. `test.skip.sequential.only.skip…` still matches `test.(skip|sequential)`).
+///
+/// Implementation collects the callee identifiers and performs a direct prefix
+/// match against the sequences above (no trie/state-machine involved).

Also applies to: 963-966

♻️ Duplicate comments (1)
crates/biome_js_syntax/src/expr_ext.rs (1)

1023-1066: Broaden .each/.for detection to computed and optional chains

At the moment this only matches JsStaticMemberExpression. Real suites also write describe["each"], test?.each, etc. Reusing AnyJsMemberExpression + member_name() keeps the logic centralised and robust.

Apply:

-    pub fn contains_a_test_each_pattern(&self) -> bool {
-        let Self::JsStaticMemberExpression(member_expression) = self else {
-            return false;
-        };
-
-        if matches!(member_expression.object(), Ok(rest) if !rest.contains_a_test_pattern()) {
-            return false;
-        }
-
-        match member_expression.member().ok() {
-            Some(AnyJsName::JsName(name)) => {
-                if let Some(token) = name
-                    .value_token()
-                    .ok()
-                    .map(|token| token.token_text_trimmed())
-                {
-                    return matches!(token.text(), "each" | "for");
-                }
-                false
-            }
-            _ => false,
-        }
-    }
+    pub fn contains_a_test_each_pattern(&self) -> bool {
+        use crate::{AnyJsMemberExpression, StaticValue};
+        let Some(member) = AnyJsMemberExpression::cast_ref(self.syntax()) else {
+            return false;
+        };
+
+        // The object must itself be a recognised test/describe pattern
+        if matches!(member.object(), Ok(rest) if !rest.contains_a_test_pattern()) {
+            return false;
+        }
+
+        // Accept `.each` / `.for` on static or computed names: obj.each, obj["each"], obj?.each
+        match member.member_name() {
+            Some(StaticValue::String(tok)) => matches!(tok.text(), "each" | "for"),
+            _ => false,
+        }
+    }

Optional: add tests for:

  • `describe["each"]```
  • `test?.only?.each```
  • `it["for"]```
🧹 Nitpick comments (2)
crates/biome_js_syntax/src/expr_ext.rs (2)

963-1021: Prefix-matching fix is sound; consider computed/optional member support next

Collecting and reversing the callee parts to do a prefix check neatly addresses >5 segment chains. One gap remains: CalleeNamesIterator skips computed members, so describe["skip"] or optional-chained segments won’t be recognised by contains_a_test_pattern().

Suggestion (non-blocking): extend CalleeNamesIterator to yield names for JsComputedMemberExpression when the index is a literal string, and to skip over optional chaining. Sketch:

// Extend Iterator::next() arm
match current {
    // existing arms...
    JsComputedMemberExpression(member) => {
        // yield "each" for obj["each"], etc.
        if let Ok(expr) = member.member() {
            let expr = expr.omit_parentheses();
            if let Some(StaticValue::String(tok)) = expr.as_static_value() {
                self.next = member.object().ok();
                return Some(tok.token_text_trimmed());
            }
        }
        None
    }
    // ...
}

That would make contains_a_test_pattern() cover bracketed/optional forms consistently with the new each/for helper (see next comment).


2029-2032: Minor readability: extract the “is test-like callee” guard

The guard mixes length and two separate callee-shapes. Consider extracting to a local is_test_callee boolean before the match for easier scanning and future extension (no behavioural change).

let is_test_callee =
    callee.contains_a_test_pattern()
    || matches!(callee,
        AnyJsExpression::JsCallExpression(inner)
            if inner.callee()?.contains_a_test_each_pattern()
    );

match (args.next(), args.next(), args.next()) {
    (Some(Ok(argument)), None, None) if arguments.args().len() == 1 => { /* ... */ }
    (Some(Ok(AnyJsCallArgument::AnyJsExpression(_))), Some(Ok(second)), third)
        if arguments.args().len() <= 3 && is_test_callee =>
    { /* ... */ }
    _ => Ok(false)
}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 310b05a and 5217d66.

📒 Files selected for processing (1)
  • crates/biome_js_syntax/src/expr_ext.rs (7 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{rs,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Format Rust and TOML files before committing (use just f/just format).

Files:

  • crates/biome_js_syntax/src/expr_ext.rs
crates/biome_*_{syntax,parser,formatter,analyze,factory,semantic}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Maintain the per-language crate structure: biome_{lang}_{syntax,parser,formatter,analyze,factory,semantic}

Files:

  • crates/biome_js_syntax/src/expr_ext.rs
crates/biome_*/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place core crates under /crates/biome_*/

Files:

  • crates/biome_js_syntax/src/expr_ext.rs
🧬 Code graph analysis (1)
crates/biome_js_syntax/src/expr_ext.rs (1)
crates/biome_js_analyze/src/lint/correctness/no_self_assign.rs (2)
  • object (575-580)
  • name (740-748)
🔇 Additional comments (3)
crates/biome_js_syntax/src/expr_ext.rs (3)

693-701: Delegation to central matcher looks good

Nice tidy-up: the template helper now defers to AnyJsExpression::contains_a_test_each_pattern(), and the doc link points to the right place.


2237-2241: Tests: good coverage for simple, deep and chained .each cases

The assertions reflect the new boolean API and demonstrate the prefix-matching behaviour on long chains. The describe.each([])(...) case is particularly helpful.

Also applies to: 2245-2247, 2251-2257, 2261-2263, 2287-2299


936-936: No internal breakages detected; bump crate semver to major

  • Ripgrep confirms no remaining contains_a_test_pattern().ok() calls and all call sites have been updated to the new -> bool signature, including in biome_js_analyze (no_skipped_tests.rs:64).
  • contains_a_test_each_pattern also consistently returns bool.
  • Since contains_a_test_pattern is part of the public API of biome_js_syntax, publishing this change requires a semver-major version bump.

@github-actions github-actions bot removed the A-Formatter Area: formatter label Aug 28, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
crates/biome_js_syntax/src/expr_ext.rs (1)

1050-1073: Support computed and optional member access for .each/.for.

At the moment this only matches JsStaticMemberExpression. Real suites also write describe["each"] and optional chains. You already have AnyJsMemberExpression::member_name(); use it here.

-    pub fn contains_a_test_each_pattern(&self) -> bool {
-        let Self::JsStaticMemberExpression(member_expression) = self else {
-            return false;
-        };
-
-        if matches!(member_expression.object(), Ok(rest) if !rest.contains_a_test_pattern()) {
-            return false;
-        }
-
-        match member_expression.member().ok() {
-            Some(AnyJsName::JsName(name)) => {
-                if let Some(token) = name
-                    .value_token()
-                    .ok()
-                    .map(|token| token.token_text_trimmed())
-                {
-                    return matches!(token.text(), "each" | "for");
-                }
-                false
-            }
-            _ => false,
-        }
-    }
+    pub fn contains_a_test_each_pattern(&self) -> bool {
+        use crate::AnyJsMemberExpression;
+        let Some(member) = AnyJsMemberExpression::cast_ref(self.syntax()) else {
+            return false;
+        };
+        if matches!(member.object(), Ok(rest) if !rest.contains_a_test_pattern()) {
+            return false;
+        }
+        match member.member_name() {
+            Some(StaticValue::String(tok)) => matches!(tok.text(), "each" | "for"),
+            _ => false,
+        }
+    }

Add tests for these cases:

#[test]
fn matches_computed_and_optional_each() {
    let t = extract_template(r#"describe["each"]``"#);
    assert!(t.is_test_each_pattern_callee());
    // Optional chaining (if parser supports it as a tag)
    // let t = extract_template(r#"describe?.each``"#);
    // assert!(t.is_test_each_pattern_callee());
}
🧹 Nitpick comments (1)
crates/biome_js_syntax/src/expr_ext.rs (1)

2243-2248: Broaden test coverage: computed/optional .each and duplicate modifiers beyond only.

Please add cases for:

  • describe["each"] and test["for"].
  • Optional member access if supported by the parser.
  • A repeated non-only modifier (e.g., test.concurrent.concurrent(name, fn)) to document intended behaviour.

I can open a follow-up with the extra tests if you like.

Also applies to: 2252-2260, 2299-2309, 2302-2309

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5217d66 and 3a2b009.

📒 Files selected for processing (1)
  • crates/biome_js_syntax/src/expr_ext.rs (8 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{rs,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Format Rust and TOML files before committing (use just f/just format).

Files:

  • crates/biome_js_syntax/src/expr_ext.rs
crates/biome_*_{syntax,parser,formatter,analyze,factory,semantic}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Maintain the per-language crate structure: biome_{lang}_{syntax,parser,formatter,analyze,factory,semantic}

Files:

  • crates/biome_js_syntax/src/expr_ext.rs
crates/biome_*/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place core crates under /crates/biome_*/

Files:

  • crates/biome_js_syntax/src/expr_ext.rs
🧠 Learnings (1)
📚 Learning: 2025-08-17T08:56:30.831Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-08-17T08:56:30.831Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/tests/quick_test.rs : Use `biome_js_analyze/tests/quick_test.rs` for quick, ad-hoc testing; un-ignore the test and adjust the rule filter as needed

Applied to files:

  • crates/biome_js_syntax/src/expr_ext.rs
🔇 Additional comments (4)
crates/biome_js_syntax/src/expr_ext.rs (4)

694-701: Good delegation and clearer docs.

Linking the tag check to contains_a_test_each_pattern() is tidy and the docstring is clear.


2036-2039: Nice: nested .each calls handled in is_test_call_expression.

Accepting call-expression callees whose own callee matches the each/for pattern is the right shape for describe.each([])(...) and friends.


2268-2272: Targeted test for repeated .only is good; keep it focused.

This will still pass if we narrow the duplicate check to only the "only" modifier as suggested.


964-974: Scope duplicate ban to only only tokens
A ripgrep scan across JS/TS files only turned up .only.only chains—no other modifiers repeat. Replace the broad duplicate check with an only_count > 1 guard and continue validating the full callee tail. Please run the no_skipped_tests and no_duplicate_test_hooks rules to verify there are no regressions.

@ematipico ematipico merged commit aa55c8d into biomejs:main Nov 11, 2025
19 checks passed
@github-actions github-actions bot mentioned this pull request Nov 11, 2025
ematipico added a commit to hamirmahal/biome that referenced this pull request Nov 19, 2025
…js#7287)

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
l0ngvh pushed a commit to l0ngvh/biome that referenced this pull request Dec 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-CLI Area: CLI A-Linter Area: linter A-Parser Area: parser L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

💅 noDuplicateTestHook produces false positives for Vitest chained describe.XYZ blocks

2 participants