Skip to content

feat(lint/js): add noIdenticalTestTitle#9376

Merged
dyc3 merged 2 commits intomainfrom
dyc3/no-identical-test-title
Apr 11, 2026
Merged

feat(lint/js): add noIdenticalTestTitle#9376
dyc3 merged 2 commits intomainfrom
dyc3/no-identical-test-title

Conversation

@dyc3
Copy link
Copy Markdown
Contributor

@dyc3 dyc3 commented Mar 7, 2026

Summary

Adds noIdenticalTestTitle

Generated by sonnet 4.6

Test Plan

added snapshot tests

Docs

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 7, 2026

🦋 Changeset detected

Latest commit: 8471364

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

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

@github-actions github-actions Bot added A-Project Area: project A-Linter Area: linter L-JavaScript Language: JavaScript and super languages A-Diagnostic Area: diagnostocis labels Mar 7, 2026
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Mar 7, 2026

Merging this PR will not alter performance

✅ 58 untouched benchmarks
⏩ 196 skipped benchmarks1


Comparing dyc3/no-identical-test-title (8471364) with main (bf40a0c)2

Open in CodSpeed

Footnotes

  1. 196 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.

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

@github-actions github-actions Bot added the A-CLI Area: CLI label Mar 7, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 7, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a nursery lint rule noIdenticalTestTitle that flags duplicate static titles among sibling describe blocks and unit-test calls at the same nesting level. Introduces a crate-level unit_tests module with is_unit_test and is_describe_call helpers, static title extraction (string literals and simple template literals), scope traversal through module/script and describe callback bodies, suppression handling, and diagnostics. Includes rule declaration, options type, and new valid/invalid test fixtures.

Suggested reviewers

  • ematipico
  • Netail
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarises the main change: adding a new lint rule called noIdenticalTestTitle for JavaScript.
Description check ✅ Passed The description is related to the changeset, mentioning the addition of noIdenticalTestTitle and noting snapshot tests were added, though it's somewhat minimal.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dyc3/no-identical-test-title

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
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: 2

🧹 Nitpick comments (2)
crates/biome_rule_options/src/no_identical_test_title.rs (1)

3-6: Add a one-line rustdoc to the options type

Even an empty options struct is part of the rule surface here, so it wants a short comment to keep the docs tidy and consistent.

As per coding guidelines, "Use inline rustdoc documentation for rules, assists, and their options".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_rule_options/src/no_identical_test_title.rs` around lines 3 - 6,
Add a one-line inline rustdoc comment to the NoIdenticalTestTitleOptions struct
to document its purpose (e.g., "Options for the no_identical_test_title rule.")
by placing a /// comment directly above the pub struct
NoIdenticalTestTitleOptions {} declaration; keep it brief and follow existing
docs style used for other rule option types.
.changeset/add-no-identical-test-title.md (1)

5-5: Trim the upstream rule references from the changeset

The behaviour description is already enough; the “porting jest/no-identical-title and vitest/no-identical-title” tail reads like implementation detail rather than release-note content.

As per coding guidelines, "Changeset descriptions must be written for end users (explain impact and what changed), not developers (avoid implementation details)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.changeset/add-no-identical-test-title.md at line 5, The changeset text
includes upstream implementation detail ("porting `jest/no-identical-title` and
`vitest/no-identical-title`") that should be removed; edit the changeset entry
for nursery/noIdenticalTestTitle to keep only the user-facing behavior
description ("This rule disallows using the same title for two `describe` blocks
or two test cases at the same nesting level.") and remove the trailing "porting
..." clause so the description targets end users rather than developers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_js_analyze/src/frameworks/unit_tests.rs`:
- Around line 24-31: The is_describe_call function currently treats any member
call ending in "describe" as a test suite (e.g., foo.describe()), which is too
permissive; change the logic in is_describe_call to first split
callee.to_trimmed_text() into first and second segments and only accept the
second == "describe" case when the first segment is a known test root (e.g.,
"test", "it", or "describe"), i.e., return true if first is one of "describe" |
"fdescribe" | "xdescribe" OR (second == Some("describe") && first is one of the
allowed roots like "test" | "it" | "describe"); update the matching expression
in is_describe_call accordingly.

In `@crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs`:
- Around line 219-230: The extraction currently ignores no-substitution template
literals, so update extract_call_title to also recognize template literal nodes
(e.g., match AnyJsExpression variants representing template literals with a
single quasi and no substitutions) and return their static inner text just like
JsStringLiteralExpression; locate the match in extract_call_title and add a
branch that extracts the template text (using the same inner_string_text() or
equivalent accessor) for single-quasi templates, then add a test fixture that
uses it(`same title`, ...) to ensure duplicate detection catches backtick
no-substitution templates.

---

Nitpick comments:
In @.changeset/add-no-identical-test-title.md:
- Line 5: The changeset text includes upstream implementation detail ("porting
`jest/no-identical-title` and `vitest/no-identical-title`") that should be
removed; edit the changeset entry for nursery/noIdenticalTestTitle to keep only
the user-facing behavior description ("This rule disallows using the same title
for two `describe` blocks or two test cases at the same nesting level.") and
remove the trailing "porting ..." clause so the description targets end users
rather than developers.

In `@crates/biome_rule_options/src/no_identical_test_title.rs`:
- Around line 3-6: Add a one-line inline rustdoc comment to the
NoIdenticalTestTitleOptions struct to document its purpose (e.g., "Options for
the no_identical_test_title rule.") by placing a /// comment directly above the
pub struct NoIdenticalTestTitleOptions {} declaration; keep it brief and follow
existing docs style used for other rule option types.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 44cd8fd8-0301-40b8-b9b2-e371fe57bf74

📥 Commits

Reviewing files that changed from the base of the PR and between 701ddd3 and ef72bdd.

⛔ Files ignored due to path filters (4)
  • crates/biome_configuration/src/analyzer/linter/rules.rs is excluded by !**/rules.rs and included by **
  • crates/biome_diagnostics_categories/src/categories.rs is excluded by !**/categories.rs and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/invalid.js.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/valid.js.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (8)
  • .changeset/add-no-identical-test-title.md
  • crates/biome_js_analyze/src/frameworks/mod.rs
  • crates/biome_js_analyze/src/frameworks/unit_tests.rs
  • crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/invalid.js
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/valid.js
  • crates/biome_rule_options/src/lib.rs
  • crates/biome_rule_options/src/no_identical_test_title.rs

Comment thread crates/biome_js_analyze/src/frameworks/unit_tests.rs Outdated
Comment thread crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs
@dyc3 dyc3 marked this pull request as draft March 7, 2026 11:04
@dyc3 dyc3 force-pushed the dyc3/no-identical-test-title branch from 0a11e3b to fe94218 Compare March 7, 2026 14:15
@dyc3 dyc3 marked this pull request as ready for review March 7, 2026 14:25
Copy link
Copy Markdown
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 (1)
crates/biome_rule_options/src/no_identical_test_title.rs (1)

3-6: Add rustdoc to the options type.

Even an empty options struct lands in the generated config/schema surface, so it should say that the rule currently has no configurable options.

♻️ Suggested tweak
+/// Options for `noIdenticalTestTitle`.
+///
+/// This rule currently has no configurable options.
 pub struct NoIdenticalTestTitleOptions {}

As per coding guidelines, "Use inline rustdoc documentation for rules, assists, and their options".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_rule_options/src/no_identical_test_title.rs` around lines 3 - 6,
Add inline rustdoc to the NoIdenticalTestTitleOptions type to document that this
rule has no configurable options; update the doc comment immediately above the
pub struct NoIdenticalTestTitleOptions {} to a concise sentence like "Options
for the `no_identical_test_title` rule — this rule has no configurable options."
so the generated config/schema surface clearly communicates there are no
options; keep the comment as normal Rust doc (///) and place it directly above
the struct definition.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_js_analyze/src/frameworks/unit_tests.rs`:
- Around line 27-77: The matcher in is_describe_call currently only accepts a
bare "describe" identifier or a single static member whose right-most segment is
"describe", so chained modifier forms like describe.only, describe.skip, and
test.describe.only fall through; update is_describe_call to unwrap
AnyJsExpression::JsStaticMemberExpression chains (traverse member().object()
recursively) to find the right-most name token and accept it if the final
identifier is "describe", then allow additional member segments after that
(e.g., "only", "skip") by treating any further static-member chain as part of
the same describe scope; reference the is_describe_call function, the callee
variable, and AnyJsExpression::JsStaticMemberExpression/member/object traversal
when implementing the change, and add snapshot tests covering describe.only,
describe.skip, test.describe.only, and similar variants under
tests/specs/{group}/{rule}/ to ensure duplicates inside these modified suites
are detected.

---

Nitpick comments:
In `@crates/biome_rule_options/src/no_identical_test_title.rs`:
- Around line 3-6: Add inline rustdoc to the NoIdenticalTestTitleOptions type to
document that this rule has no configurable options; update the doc comment
immediately above the pub struct NoIdenticalTestTitleOptions {} to a concise
sentence like "Options for the `no_identical_test_title` rule — this rule has no
configurable options." so the generated config/schema surface clearly
communicates there are no options; keep the comment as normal Rust doc (///) and
place it directly above the struct definition.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8660b115-0685-46c3-adb5-6633c5a08ffb

📥 Commits

Reviewing files that changed from the base of the PR and between ef72bdd and fe94218.

⛔ Files ignored due to path filters (8)
  • crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs is excluded by !**/migrate/eslint_any_rule_to_biome.rs and included by **
  • crates/biome_configuration/src/analyzer/linter/rules.rs is excluded by !**/rules.rs and included by **
  • crates/biome_configuration/src/generated/domain_selector.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_diagnostics_categories/src/categories.rs is excluded by !**/categories.rs and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/invalid.js.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/valid.js.snap is excluded by !**/*.snap and included by **
  • packages/@biomejs/backend-jsonrpc/src/workspace.ts is excluded by !**/backend-jsonrpc/src/workspace.ts and included by **
  • packages/@biomejs/biome/configuration_schema.json is excluded by !**/configuration_schema.json and included by **
📒 Files selected for processing (8)
  • .changeset/add-no-identical-test-title.md
  • crates/biome_js_analyze/src/frameworks/mod.rs
  • crates/biome_js_analyze/src/frameworks/unit_tests.rs
  • crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/invalid.js
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/valid.js
  • crates/biome_rule_options/src/lib.rs
  • crates/biome_rule_options/src/no_identical_test_title.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/biome_js_analyze/src/frameworks/mod.rs

Comment on lines +27 to +77
pub(crate) fn is_describe_call(call: &JsCallExpression) -> bool {
let Ok(callee) = call.callee() else {
return false;
};
let callee = callee.omit_parentheses();

match callee {
// describe(...) / fdescribe(...) / xdescribe(...)
AnyJsExpression::JsIdentifierExpression(ident) => ident
.name()
.and_then(|r| r.value_token())
.is_ok_and(|tok| matches!(tok.text_trimmed(), "describe" | "fdescribe" | "xdescribe")),

// test.describe(...) / it.describe(...) / describe.each(...) etc.
AnyJsExpression::JsStaticMemberExpression(member) => {
// The right-hand member must be "describe".
let member_is_describe = member
.member()
.ok()
.and_then(|m| {
if let AnyJsName::JsName(n) = m {
Some(n)
} else {
None
}
})
.and_then(|n| n.value_token().ok())
.is_some_and(|tok| tok.text_trimmed() == "describe");

if !member_is_describe {
return false;
}

// The left-hand object must be a known test root identifier.
member
.object()
.ok()
.map(|o| o.omit_parentheses())
.and_then(|o| {
if let AnyJsExpression::JsIdentifierExpression(i) = o {
Some(i)
} else {
None
}
})
.and_then(|i| i.name().and_then(|r| r.value_token()).ok())
.is_some_and(|tok| matches!(tok.text_trimmed(), "test" | "it" | "describe"))
}

_ => false,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Recognise chained describe variants before classifying tests.

describe.only(...), describe.skip(...), and test.describe.only(...) all fall through here because the matcher only accepts bare describe(...) or a single static member whose last segment is describe. Those suites are then skipped entirely or treated as leaf tests, so duplicate titles inside them are missed. Please unwrap the static-member chain and treat modifier forms as describe scopes, then add coverage for the modifier variants.

Based on learnings, "All code changes MUST include tests - lint rules need snapshot tests in tests/specs/{group}/{rule}/, formatter changes need snapshot tests with valid/invalid cases, parser changes need test files covering valid and error cases".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/frameworks/unit_tests.rs` around lines 27 - 77,
The matcher in is_describe_call currently only accepts a bare "describe"
identifier or a single static member whose right-most segment is "describe", so
chained modifier forms like describe.only, describe.skip, and test.describe.only
fall through; update is_describe_call to unwrap
AnyJsExpression::JsStaticMemberExpression chains (traverse member().object()
recursively) to find the right-most name token and accept it if the final
identifier is "describe", then allow additional member segments after that
(e.g., "only", "skip") by treating any further static-member chain as part of
the same describe scope; reference the is_describe_call function, the callee
variable, and AnyJsExpression::JsStaticMemberExpression/member/object traversal
when implementing the change, and add snapshot tests covering describe.only,
describe.skip, test.describe.only, and similar variants under
tests/specs/{group}/{rule}/ to ensure duplicates inside these modified suites
are detected.

@dyc3 dyc3 force-pushed the dyc3/no-identical-test-title branch from fe94218 to 2192676 Compare March 8, 2026 16:15
Copy link
Copy Markdown
Contributor Author

dyc3 commented Mar 8, 2026

Copy link
Copy Markdown
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.

♻️ Duplicate comments (1)
crates/biome_js_analyze/src/frameworks/unit_tests.rs (1)

27-78: ⚠️ Potential issue | 🟠 Major

Handle chained describe modifiers before classifying the call.

This still only recognises bare describe(...) or a single static member ending in describe. describe.only(...), describe.skip(...), and test.describe.only(...) will fall through here, so those scopes are skipped entirely or treated as leaf tests, and duplicate titles inside them get missed. Please unwrap the static-member chain and add snapshot coverage for the modifier variants while you’re there.

Based on learnings, "All code changes MUST include tests - lint rules need snapshot tests in tests/specs/{group}/{rule}/".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/frameworks/unit_tests.rs` around lines 27 - 78,
is_describe_call currently only matches bare describe(...) or a single
JsStaticMemberExpression ending in "describe" and thus misses chained modifiers
like describe.only(...), describe.skip(...), or test.describe.only(...); update
is_describe_call to walk/unwind a chain of JsStaticMemberExpression on the
callee (peeling right-hand members) until you find the terminal name token and
confirm that one of the identifiers in the chain is "describe" and that any
preceding root object is one of the known test roots ("test", "it", "describe");
treat modifier names ("only", "skip", "each", etc.) as valid intermediate
members rather than failing the match; update the logic in is_describe_call (and
where it checks member().object() and member().member()) to traverse nested
JsStaticMemberExpression nodes, and add snapshot tests under
tests/specs/{group}/{rule}/ covering describe.only, describe.skip,
describe.each, test.describe.only, and chained combinations so lint rules detect
duplicates inside these modified describe blocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@crates/biome_js_analyze/src/frameworks/unit_tests.rs`:
- Around line 27-78: is_describe_call currently only matches bare describe(...)
or a single JsStaticMemberExpression ending in "describe" and thus misses
chained modifiers like describe.only(...), describe.skip(...), or
test.describe.only(...); update is_describe_call to walk/unwind a chain of
JsStaticMemberExpression on the callee (peeling right-hand members) until you
find the terminal name token and confirm that one of the identifiers in the
chain is "describe" and that any preceding root object is one of the known test
roots ("test", "it", "describe"); treat modifier names ("only", "skip", "each",
etc.) as valid intermediate members rather than failing the match; update the
logic in is_describe_call (and where it checks member().object() and
member().member()) to traverse nested JsStaticMemberExpression nodes, and add
snapshot tests under tests/specs/{group}/{rule}/ covering describe.only,
describe.skip, describe.each, test.describe.only, and chained combinations so
lint rules detect duplicates inside these modified describe blocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 361946f2-0a7f-4a9c-b183-bcf890c73943

📥 Commits

Reviewing files that changed from the base of the PR and between fe94218 and 2192676.

⛔ Files ignored due to path filters (8)
  • crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs is excluded by !**/migrate/eslint_any_rule_to_biome.rs and included by **
  • crates/biome_configuration/src/analyzer/linter/rules.rs is excluded by !**/rules.rs and included by **
  • crates/biome_configuration/src/generated/domain_selector.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_diagnostics_categories/src/categories.rs is excluded by !**/categories.rs and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/invalid.js.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/valid.js.snap is excluded by !**/*.snap and included by **
  • packages/@biomejs/backend-jsonrpc/src/workspace.ts is excluded by !**/backend-jsonrpc/src/workspace.ts and included by **
  • packages/@biomejs/biome/configuration_schema.json is excluded by !**/configuration_schema.json and included by **
📒 Files selected for processing (8)
  • .changeset/add-no-identical-test-title.md
  • crates/biome_js_analyze/src/frameworks/mod.rs
  • crates/biome_js_analyze/src/frameworks/unit_tests.rs
  • crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/invalid.js
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/valid.js
  • crates/biome_rule_options/src/lib.rs
  • crates/biome_rule_options/src/no_identical_test_title.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/valid.js
  • .changeset/add-no-identical-test-title.md

@dyc3 dyc3 requested review from a team March 8, 2026 16:37
Copy link
Copy Markdown
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

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

Wacky query to fix

Comment thread crates/biome_js_analyze/src/frameworks/unit_tests.rs Outdated
Comment thread crates/biome_js_analyze/src/frameworks/unit_tests.rs Outdated
Comment thread crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs
declare_node_union! {
/// A node that represents a scope containing test/describe calls:
/// either the top-level module/script, or a `describe(...)` call.
pub AnyTestScope = JsModule | JsScript | JsCallExpression
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Wacky query again

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

My thought process here was that this would let us avoid traversing the syntax tree inside the rule, and let the analyzer just do the (ideally) single traversal as its querying. I suppose the rule could just query for JsStatementList instead and filter for the lists we want, but my hunch is that would activate the rule more often on average.

Copy link
Copy Markdown
Member

@ematipico ematipico Mar 9, 2026

Choose a reason for hiding this comment

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

My thought process here was that this would let us avoid traversing the syntax tree inside the rule

By default, the rule doesn't do it, but you can tell the rule to do it:

/// Allows the rule to suppress a set of syntax nodes to prevent them from
/// matching the `Query`. This is useful for rules that implement a code
/// action that recursively modifies multiple nodes at once, this hook
/// allows these rules to avoid matching on those nodes again.
///
/// # Example
///
/// ```ignore
/// impl Rule for SimplifyExpression {
/// type Query = BinaryExpression;
///
/// fn run(ctx: &RuleContext<Self>) -> Self::Signals {
/// // Recursively check this expression and its children for simplification
/// // opportunities
/// check_can_simplify(ctx.query())
/// }
///
/// fn suppressed_nodes(
/// _ctx: &RuleContext<Self>,
/// state: &Self::State,
/// suppressions: &mut RuleSuppressions<RuleLanguage<Self>>
/// ) {
/// // Prevent this rule from matching again on nodes that were already checked by
/// // `check_can_simplify`
/// for node in &state.nodes {
/// suppressions.suppress_node(node.clone());
/// }
/// }
/// }
/// ```
fn suppressed_nodes(
ctx: &RuleContext<Self>,
state: &Self::State,
suppressions: &mut RuleSuppressions<RuleLanguage<Self>>,
) {
let (..) = (ctx, state, suppressions);

Copy link
Copy Markdown
Contributor Author

@dyc3 dyc3 Mar 24, 2026

Choose a reason for hiding this comment

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

Is something like this what you had in mind? 8c1ccf4 There aren't any rules that use this feature

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It was initially added for useTemplate to not trigger the rule in nested templates. The rule changed since then. And yes, that's essentially how you would use it

Comment thread crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs Outdated
@dyc3 dyc3 force-pushed the dyc3/no-identical-test-title branch 2 times, most recently from a337d77 to 806a0fe Compare March 9, 2026 15:01
Copy link
Copy Markdown
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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_js_analyze/src/frameworks/unit_tests.rs`:
- Around line 20-26: The docstring falsely claims support for
describe.each(...); fix by updating the matching logic used where
member_is_describe is checked so JsStaticMemberExpression callees also accept
the "each" modifier chain: treat a callee as a describe when either the property
name is "describe" OR the property name is "each" and its object is the
identifier or static member whose final property is "describe" (i.e. allow
patterns like describe.each(...) and test.describe.each(...)); update the check
in the function that uses member_is_describe to recognize this modifier chain
(or alternatively change the docstring to remove the claim if you prefer not to
implement the modifier handling).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 859a77a5-61d0-4a81-82d6-3c7f1dfb2231

📥 Commits

Reviewing files that changed from the base of the PR and between 2192676 and 806a0fe.

⛔ Files ignored due to path filters (8)
  • crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs is excluded by !**/migrate/eslint_any_rule_to_biome.rs and included by **
  • crates/biome_configuration/src/analyzer/linter/rules.rs is excluded by !**/rules.rs and included by **
  • crates/biome_configuration/src/generated/domain_selector.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_diagnostics_categories/src/categories.rs is excluded by !**/categories.rs and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/invalid.js.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/valid.js.snap is excluded by !**/*.snap and included by **
  • packages/@biomejs/backend-jsonrpc/src/workspace.ts is excluded by !**/backend-jsonrpc/src/workspace.ts and included by **
  • packages/@biomejs/biome/configuration_schema.json is excluded by !**/configuration_schema.json and included by **
📒 Files selected for processing (8)
  • .changeset/add-no-identical-test-title.md
  • crates/biome_js_analyze/src/frameworks/mod.rs
  • crates/biome_js_analyze/src/frameworks/unit_tests.rs
  • crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/invalid.js
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/valid.js
  • crates/biome_rule_options/src/lib.rs
  • crates/biome_rule_options/src/no_identical_test_title.rs
🚧 Files skipped from review as they are similar to previous changes (6)
  • crates/biome_js_analyze/src/frameworks/mod.rs
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/valid.js
  • crates/biome_rule_options/src/no_identical_test_title.rs
  • crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs
  • .changeset/add-no-identical-test-title.md
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/invalid.js

Comment thread crates/biome_js_analyze/src/frameworks/unit_tests.rs
Copy link
Copy Markdown
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 (1)
crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs (1)

175-209: Consider HashSet for O(1) duplicate lookups.

Using Vec::contains for describe_titles and test_titles gives O(n) per lookup, yielding O(n²) overall for scopes with many tests. For most real-world test files this is likely fine, but a HashSet<TokenText> would scale better.

♻️ Suggested refactor
+use std::collections::HashSet;
+
 fn scan_scope(statements: &[AnyJsStatement]) -> ScopeScanResult {
-    let mut describe_titles = vec![];
-    let mut test_titles = vec![];
+    let mut describe_titles: HashSet<TokenText> = HashSet::new();
+    let mut test_titles: HashSet<TokenText> = HashSet::new();
     let mut duplicates = vec![];
     let mut suppressed_scopes = vec![];

     for stmt in statements {
         // ...
         if is_describe_call(&call) {
             if let Some(title) = extract_call_title(&call) {
-                if describe_titles.contains(&title) {
+                if !describe_titles.insert(title) {
                     duplicates.push(call.clone());
-                } else {
-                    describe_titles.push(title);
                 }
             }
             // ...
         } else if is_unit_test(&call)
             && let Some(title) = extract_call_title(&call)
         {
-            if test_titles.contains(&title) {
+            if !test_titles.insert(title) {
                 duplicates.push(call);
-            } else {
-                test_titles.push(title);
             }
         }
     }
     // ...
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs` around
lines 175 - 209, Replace the O(n) Vec lookups for duplicate title detection with
a HashSet: change describe_titles and test_titles to HashSet<TokenText> (or the
actual title type), import std::collections::HashSet, and when you extract a
title in scan_scope use set.insert(title.clone()) (or check the return value) to
determine if it was already present and then push the call into duplicates;
remove the Vec::contains checks and the pushes that add titles to the vecs and
instead insert into the corresponding HashSet; keep duplicates and
suppressed_scopes as Vecs and preserve the behavior around
describe_body_statements, call_from_statement, is_describe_call, is_unit_test,
extract_call_title, and scan_scope while ensuring you clone or borrow titles as
needed to satisfy ownership.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs`:
- Around line 218-229: The code currently maintains a redundant suppression
mechanism (suppressed_scopes / suppressed_nodes) that hides nodes which never
produce signals because scope_statements_for_call already requires a JsScript
parent and nested describes cannot satisfy that; remove the suppressed_scopes
and suppressed_nodes data structures and all code that populates or checks them
(references to suppressed_scopes, suppressed_nodes, and any early returns or
skips based on them) and simplify the signal-generation flow to rely solely on
scope_statements_for_call (and functions it calls like module_statements) so
nodes that naturally return None are simply not signaled; ensure related tests
or comments that referenced suppression are updated or removed.

---

Nitpick comments:
In `@crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs`:
- Around line 175-209: Replace the O(n) Vec lookups for duplicate title
detection with a HashSet: change describe_titles and test_titles to
HashSet<TokenText> (or the actual title type), import std::collections::HashSet,
and when you extract a title in scan_scope use set.insert(title.clone()) (or
check the return value) to determine if it was already present and then push the
call into duplicates; remove the Vec::contains checks and the pushes that add
titles to the vecs and instead insert into the corresponding HashSet; keep
duplicates and suppressed_scopes as Vecs and preserve the behavior around
describe_body_statements, call_from_statement, is_describe_call, is_unit_test,
extract_call_title, and scan_scope while ensuring you clone or borrow titles as
needed to satisfy ownership.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 83256805-b7a9-4f38-b7f3-6277a20bc221

📥 Commits

Reviewing files that changed from the base of the PR and between 806a0fe and 8c1ccf4.

⛔ Files ignored due to path filters (2)
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/invalid.js.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/valid.js.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (3)
  • crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/invalid.js
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/valid.js
✅ Files skipped from review due to trivial changes (2)
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/invalid.js
  • crates/biome_js_analyze/tests/specs/nursery/noIdenticalTestTitle/valid.js

Comment on lines +218 to +229
fn scope_statements_for_call(call: &JsCallExpression) -> Option<Vec<AnyJsStatement>> {
let expr_stmt = call.parent::<JsExpressionStatement>()?;

if let Some(module_items) = expr_stmt.parent::<JsModuleItemList>() {
return module_statements(&module_items);
}

let statement_list = expr_stmt.parent::<JsStatementList>()?;

statement_list.parent::<JsScript>()?;
Some(statement_list.into_iter().collect())
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether nested describe calls can ever satisfy scope_statements_for_call
# by checking for JsStatementList parents other than JsScript/JsModule in the codebase.

rg -n "JsStatementList" crates/biome_js_syntax/src/generated/nodes.rs | head -40

Repository: biomejs/biome

Length of output: 1007


🏁 Script executed:

#!/bin/bash
# Read the file to understand the suppression mechanism and context
wc -l crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs

Repository: biomejs/biome

Length of output: 128


🏁 Script executed:

#!/bin/bash
# View the suppression mechanism at lines 133-141 and 195-199
sed -n '125,210p' crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs

Repository: biomejs/biome

Length of output: 2670


🏁 Script executed:

#!/bin/bash
# View more context around the function at lines 218-229
sed -n '210,250p' crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs

Repository: biomejs/biome

Length of output: 1283


🏁 Script executed:

#!/bin/bash
# Find where scope_statements_for_call is used
rg -n "scope_statements_for_call" crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs

Repository: biomejs/biome

Length of output: 221


🏁 Script executed:

#!/bin/bash
# Find the describe_body_statements function definition
rg -n "describe_body_statements" crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs -A 8

Repository: biomejs/biome

Length of output: 1018


🏁 Script executed:

#!/bin/bash
# View the beginning of the file to see the rule structure and entry point
head -130 crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs

Repository: biomejs/biome

Length of output: 3936


🏁 Script executed:

#!/bin/bash
# Verify JsStatementList parent possibilities in nested contexts
# Check what can be parent of JsStatementList besides JsScript
rg -n "pub.*statements.*JsStatementList" crates/biome_js_syntax/src/generated/nodes.rs | head -20

Repository: biomejs/biome

Length of output: 442


🏁 Script executed:

#!/bin/bash
# Get context around JsStatementList declarations to identify parent node types
sed -n '625,650p' crates/biome_js_syntax/src/generated/nodes.rs && \
echo "---" && \
sed -n '2964,2990p' crates/biome_js_syntax/src/generated/nodes.rs && \
echo "---" && \
sed -n '5911,5940p' crates/biome_js_syntax/src/generated/nodes.rs && \
echo "---" && \
sed -n '6255,6280p' crates/biome_js_syntax/src/generated/nodes.rs

Repository: biomejs/biome

Length of output: 3386


🏁 Script executed:

#!/bin/bash
# Check if there are test cases that verify nested describe handling
fd -e rs "no_identical_test_title" crates/biome_js_analyze/tests --exec head -5 {} \;

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Find test files for this rule
find crates -name "*no_identical_test_title*" -type f

Repository: biomejs/biome

Length of output: 181


🏁 Script executed:

#!/bin/bash
# Search for any git history or comments explaining the suppression mechanism
git log --oneline -S "suppressed_scopes" -- crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs | head -5

Repository: biomejs/biome

Length of output: 107


🏁 Script executed:

#!/bin/bash
# Check if there are inline comments near the suppression code
sed -n '180,210p' crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs

Repository: biomejs/biome

Length of output: 1109


🏁 Script executed:

#!/bin/bash
# View the suppressed_nodes callback to confirm it only processes items never reached
sed -n '138,143p' crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs

Repository: biomejs/biome

Length of output: 275


🏁 Script executed:

#!/bin/bash
# Verify the parent check once more - confirm JsBlockStatement won't satisfy statement_list.parent::<JsScript>()
sed -n '218,228p' crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs

Repository: biomejs/biome

Length of output: 489


Remove the redundant suppression mechanism.

You're correct—scope_statements_for_call requires a JsScript parent, which nested describes (parented by JsFunctionBody or JsBlockStatement) can never satisfy. They naturally return None and skip signal generation entirely. The suppressed_scopes / suppressed_nodes machinery (lines 133–141, 195–199) suppresses nodes that never generate signals in the first place, making it unnecessary. Consider removing it to simplify the implementation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/nursery/no_identical_test_title.rs` around
lines 218 - 229, The code currently maintains a redundant suppression mechanism
(suppressed_scopes / suppressed_nodes) that hides nodes which never produce
signals because scope_statements_for_call already requires a JsScript parent and
nested describes cannot satisfy that; remove the suppressed_scopes and
suppressed_nodes data structures and all code that populates or checks them
(references to suppressed_scopes, suppressed_nodes, and any early returns or
skips based on them) and simplify the signal-generation flow to rely solely on
scope_statements_for_call (and functions it calls like module_statements) so
nodes that naturally return None are simply not signaled; ensure related tests
or comments that referenced suppression are updated or removed.

@dyc3 dyc3 requested review from a team and ematipico March 24, 2026 17:36
@dyc3

This comment was marked as resolved.

@dyc3 dyc3 marked this pull request as draft March 24, 2026 17:39
@dyc3 dyc3 force-pushed the dyc3/no-identical-test-title branch from 8c1ccf4 to 11d89d9 Compare March 24, 2026 17:48
@dyc3 dyc3 marked this pull request as ready for review March 24, 2026 19:46
Copy link
Copy Markdown
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

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

Looks good. Just a nit: let's docstrings the methods

Comment thread .changeset/add-no-identical-test-title.md Outdated
@dyc3 dyc3 force-pushed the dyc3/no-identical-test-title branch from 8c164d1 to aafa435 Compare April 11, 2026 12:53
@dyc3 dyc3 force-pushed the dyc3/no-identical-test-title branch from aafa435 to 8981fe2 Compare April 11, 2026 13:50
@dyc3 dyc3 merged commit 9701a33 into main Apr 11, 2026
22 checks passed
@dyc3 dyc3 deleted the dyc3/no-identical-test-title branch April 11, 2026 14:24
@github-actions github-actions Bot mentioned this pull request Apr 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-CLI Area: CLI A-Diagnostic Area: diagnostocis A-Linter Area: linter A-Project Area: project L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants