Skip to content

perf(lint): avoid string allocations in some lint rules#10088

Merged
dyc3 merged 1 commit intomainfrom
dyc3/reduce-string-allocations
Apr 22, 2026
Merged

perf(lint): avoid string allocations in some lint rules#10088
dyc3 merged 1 commit intomainfrom
dyc3/reduce-string-allocations

Conversation

@dyc3
Copy link
Copy Markdown
Contributor

@dyc3 dyc3 commented Apr 22, 2026

Summary

A bunch of our older rules allocate strings more than they should. This is a quick attempt to resolve that for some of them. If there's a perf improvement, I'll add a changeset. There's also some cosmetic refactors to slightly realign some of these rules with our current patterns.

Generated initially by gpt 5.4, but i tweaked some things by hand.

Test Plan

no snapshot updates.

Docs

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 22, 2026

⚠️ No Changeset found

Latest commit: 95bdc33

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@github-actions github-actions Bot added A-Linter Area: linter L-JavaScript Language: JavaScript and super languages L-CSS Language: CSS and super languages labels Apr 22, 2026
@dyc3 dyc3 force-pushed the dyc3/reduce-string-allocations branch from 8665ee5 to 4f1f22e Compare April 22, 2026 20:38
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 22, 2026

Merging this PR will not alter performance

✅ 87 untouched benchmarks
⏩ 167 skipped benchmarks1


Comparing dyc3/reduce-string-allocations (95bdc33) with main (5e2f90c)2

Open in CodSpeed

Footnotes

  1. 167 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 (38deeae) during the generation of this report, so 5e2f90c was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@dyc3 dyc3 force-pushed the dyc3/reduce-string-allocations branch from 4f1f22e to 95bdc33 Compare April 22, 2026 20:51
@dyc3 dyc3 marked this pull request as ready for review April 22, 2026 21:44
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

Walkthrough

This PR refactors lint rules across multiple modules to reduce string allocations in internal state. Changes consistently replace owned strings (String, Box<str>) and reconstructed text with lightweight references: TokenText, &'static str, JsSyntaxToken, and ResolvedPath. State now stores syntax nodes or token references directly, deferring text extraction and diagnostic message construction until needed during diagnostic generation or fix actions. Similar patterns are applied across CSS and JavaScript linters.

Possibly related PRs

Suggested reviewers

  • ematipico
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarises the main objective: reducing string allocations in lint rules through targeted performance optimisations.
Description check ✅ Passed The description clearly explains the motivation (reducing string allocations in older rules), acknowledges AI assistance, and notes no snapshot updates were made.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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/reduce-string-allocations

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.

Caution

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

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

56-130: ⚠️ Potential issue | 🟠 Major

Please add snapshot coverage for this rule rewrite.

This changes core duplicate-grouping behaviour (including namespaced keying), but there are no snapshot updates in the PR metadata. Please add/refresh snapshots under tests/specs/suspicious/noDuplicateJsxProps/ (including namespaced and spread-adjacent cases) to lock behaviour.

As per coding guidelines: “All code changes MUST include appropriate tests: lint rules require 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/lint/suspicious/no_duplicate_jsx_props.rs` around
lines 56 - 130, The PR changed duplicate-grouping behavior in run and the
attribute keying in attribute_name_key (including namespace handling and
interactions with adjacent spreads) but no snapshot tests were added/updated;
add or refresh snapshot tests under tests/specs/suspicious/noDuplicateJsxProps/
that exercise plain duplicate props, namespaced props, and cases with spread
props adjacent to duplicates to lock the new behavior, using the rule's existing
test harness so the snapshots capture the diagnostic output produced by run and
diagnostic for these variants.
crates/biome_js_analyze/src/lint/correctness/no_undeclared_variables.rs (1)

65-141: ⚠️ Potential issue | 🟠 Major

Add snapshot coverage for this rule refactor.

This rule’s diagnostic pipeline changed, but the PR metadata says no snapshot updates were included. Please add/refresh snapshots for noUndeclaredVariables to guard against behavioural drift.

As per coding guidelines: "All code changes MUST include appropriate tests: lint rules require 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/lint/correctness/no_undeclared_variables.rs`
around lines 65 - 141, The PR refactored the NoUndeclaredVariables rule (impl
NoUndeclaredVariables, run and diagnostic) but did not update snapshot tests;
add or refresh snapshot tests for the noUndeclaredVariables rule under
tests/specs/correctness/noUndeclaredVariables (or the existing group folder) to
cover the changed diagnostic messages and token locations produced by
NoUndeclaredVariables::run and NoUndeclaredVariables::diagnostic; run the test
harness to regenerate snapshots and commit the updated snapshot files so CI can
validate there is no unintended behavioral drift.
crates/biome_js_analyze/src/lint/complexity/no_useless_undefined_initialization.rs (1)

116-126: ⚠️ Potential issue | 🟡 Minor

Filter non-identifier declarators before pushing signals

At Line 126, run pushes declarators even when the binding is not an identifier; those are then dropped in diagnostic (Lines 135-141). That creates non-actionable signals and a bit of extra churn in a perf-focused PR.

Suggested tweak
-                let is_exported = decl
-                    .id()
-                    .ok()
-                    .and_then(|id| id.as_any_js_binding()?.as_js_identifier_binding().cloned())
-                    .is_some_and(|binding| binding.is_exported(model));
-
-                if is_exported {
+                let Some(binding) = decl
+                    .id()
+                    .ok()
+                    .and_then(|id| id.as_any_js_binding()?.as_js_identifier_binding().cloned())
+                else {
+                    continue;
+                };
+
+                if binding.is_exported(model) {
                     continue;
                 }
 
                 signals.push(decl);

Also applies to: 135-141

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

In
`@crates/biome_js_analyze/src/lint/complexity/no_useless_undefined_initialization.rs`
around lines 116 - 126, The code currently pushes declarators (decl) into
signals inside run even when the binding isn't an identifier, causing diagnostic
to later drop them; update the run logic to only push declarators whose binding
is an identifier by checking decl.id().ok().and_then(|id|
id.as_any_js_binding()?.as_js_identifier_binding()).is_some() (reusing the same
predicate used for is_exported) before signals.push(decl); similarly ensure
diagnostic (or any other consumer) no longer needs to filter non-identifier
entries so signals only contains identifier bindings.
🧹 Nitpick comments (2)
crates/biome_js_analyze/src/lint/suspicious/no_import_cycles.rs (1)

208-210: Defensive handling noted.

The continue here is defensive — based on the code flow, all ResolvedPath objects in state should have valid paths (validated before being added in find_cycle). If this ever triggers unexpectedly, the diagnostic would silently skip a path in the cycle chain, which might confuse users. Consider whether a debug assertion or logging would help catch this edge case during development.

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

In `@crates/biome_js_analyze/src/lint/suspicious/no_import_cycles.rs` around lines
208 - 210, The current loop silently continues when path.as_path() returns None
on a ResolvedPath, which can hide unexpected invalid state; instead of silently
skipping, replace the bare continue with a defensive check that surfaces the
issue during development—use a debug assertion or logging inside the loop (e.g.,
debug_assert!(path.as_path().is_some(), "ResolvedPath in find_cycle must have a
valid path") or process_logger.warn/error with context) referencing the
ResolvedPath value and the find_cycle flow so developers can detect and
investigate if this branch is ever hit; keep behavior in release builds
unchanged (i.e., still skip if None) but ensure developers get notified in
debug/testing.
crates/biome_js_analyze/src/lint/suspicious/no_duplicate_jsx_props.rs (1)

67-94: Nested duplicate scan makes this path O(n²).

Nice allocation work, but this double loop plus processed_names.contains re-scans attributes repeatedly. A single-pass grouping by AttributeNameKey keeps the same semantics with linear behaviour.

Possible linear-pass refactor
-        let mut duplicated_attributes = Vec::new();
-        let mut processed_names = Vec::new();
-
-        for (index, attr) in attributes.iter().enumerate() {
-            let Some(name) = attribute_name_key(attr) else {
-                continue;
-            };
-
-            if processed_names.contains(&name) {
-                continue;
-            }
-
-            let mut duplicates = vec![attr.clone()];
-
-            for other in attributes.iter().skip(index + 1) {
-                let Some(other_name) = attribute_name_key(other) else {
-                    continue;
-                };
-
-                if name == other_name {
-                    duplicates.push(other.clone());
-                }
-            }
-
-            if duplicates.len() > 1 {
-                processed_names.push(name);
-                duplicated_attributes.push(duplicates);
-            }
-        }
-
-        duplicated_attributes
+        use rustc_hash::FxHashMap;
+
+        let mut by_name: FxHashMap<AttributeNameKey, Vec<JsxAttribute>> = FxHashMap::default();
+        for attr in attributes {
+            let Some(name) = attribute_name_key(&attr) else {
+                continue;
+            };
+            by_name.entry(name).or_default().push(attr);
+        }
+
+        by_name
+            .into_values()
+            .filter(|group| group.len() > 1)
+            .collect()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/suspicious/no_duplicate_jsx_props.rs` around
lines 67 - 94, The current nested loop in the block that builds
duplicated_attributes (using processed_names and attribute_name_key over
attributes) is O(n²); change it to a single-pass grouping by AttributeNameKey:
iterate once over attributes, call attribute_name_key(attr) and push
attr.clone() into a HashMap<AttributeNameKey, Vec<Attribute>> (preserving
original order if needed), then collect map values with length > 1 into
duplicated_attributes; remove processed_names and the inner .contains/.skip
logic and keep all references to attribute_name_key, duplicated_attributes,
attributes, and duplicates consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@crates/biome_js_analyze/src/lint/complexity/no_useless_undefined_initialization.rs`:
- Around line 116-126: The code currently pushes declarators (decl) into signals
inside run even when the binding isn't an identifier, causing diagnostic to
later drop them; update the run logic to only push declarators whose binding is
an identifier by checking decl.id().ok().and_then(|id|
id.as_any_js_binding()?.as_js_identifier_binding()).is_some() (reusing the same
predicate used for is_exported) before signals.push(decl); similarly ensure
diagnostic (or any other consumer) no longer needs to filter non-identifier
entries so signals only contains identifier bindings.

In `@crates/biome_js_analyze/src/lint/correctness/no_undeclared_variables.rs`:
- Around line 65-141: The PR refactored the NoUndeclaredVariables rule (impl
NoUndeclaredVariables, run and diagnostic) but did not update snapshot tests;
add or refresh snapshot tests for the noUndeclaredVariables rule under
tests/specs/correctness/noUndeclaredVariables (or the existing group folder) to
cover the changed diagnostic messages and token locations produced by
NoUndeclaredVariables::run and NoUndeclaredVariables::diagnostic; run the test
harness to regenerate snapshots and commit the updated snapshot files so CI can
validate there is no unintended behavioral drift.

In `@crates/biome_js_analyze/src/lint/suspicious/no_duplicate_jsx_props.rs`:
- Around line 56-130: The PR changed duplicate-grouping behavior in run and the
attribute keying in attribute_name_key (including namespace handling and
interactions with adjacent spreads) but no snapshot tests were added/updated;
add or refresh snapshot tests under tests/specs/suspicious/noDuplicateJsxProps/
that exercise plain duplicate props, namespaced props, and cases with spread
props adjacent to duplicates to lock the new behavior, using the rule's existing
test harness so the snapshots capture the diagnostic output produced by run and
diagnostic for these variants.

---

Nitpick comments:
In `@crates/biome_js_analyze/src/lint/suspicious/no_duplicate_jsx_props.rs`:
- Around line 67-94: The current nested loop in the block that builds
duplicated_attributes (using processed_names and attribute_name_key over
attributes) is O(n²); change it to a single-pass grouping by AttributeNameKey:
iterate once over attributes, call attribute_name_key(attr) and push
attr.clone() into a HashMap<AttributeNameKey, Vec<Attribute>> (preserving
original order if needed), then collect map values with length > 1 into
duplicated_attributes; remove processed_names and the inner .contains/.skip
logic and keep all references to attribute_name_key, duplicated_attributes,
attributes, and duplicates consistent.

In `@crates/biome_js_analyze/src/lint/suspicious/no_import_cycles.rs`:
- Around line 208-210: The current loop silently continues when path.as_path()
returns None on a ResolvedPath, which can hide unexpected invalid state; instead
of silently skipping, replace the bare continue with a defensive check that
surfaces the issue during development—use a debug assertion or logging inside
the loop (e.g., debug_assert!(path.as_path().is_some(), "ResolvedPath in
find_cycle must have a valid path") or process_logger.warn/error with context)
referencing the ResolvedPath value and the find_cycle flow so developers can
detect and investigate if this branch is ever hit; keep behavior in release
builds unchanged (i.e., still skip if None) but ensure developers get notified
in debug/testing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 27f5153b-e79c-4c25-a455-689880d2cd6a

📥 Commits

Reviewing files that changed from the base of the PR and between 5e2f90c and 95bdc33.

📒 Files selected for processing (10)
  • crates/biome_css_analyze/src/lint/suspicious/no_duplicate_custom_properties.rs
  • crates/biome_js_analyze/src/lint/complexity/no_useless_undefined_initialization.rs
  • crates/biome_js_analyze/src/lint/correctness/no_global_dirname_filename.rs
  • crates/biome_js_analyze/src/lint/correctness/no_undeclared_variables.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicated_spread_props.rs
  • crates/biome_js_analyze/src/lint/style/no_restricted_globals.rs
  • crates/biome_js_analyze/src/lint/suspicious/no_alert.rs
  • crates/biome_js_analyze/src/lint/suspicious/no_duplicate_jsx_props.rs
  • crates/biome_js_analyze/src/lint/suspicious/no_import_cycles.rs
  • crates/biome_js_analyze/src/syntax/correctness/no_duplicate_private_class_members.rs

@dyc3 dyc3 merged commit f74b63a into main Apr 22, 2026
32 checks passed
@dyc3 dyc3 deleted the dyc3/reduce-string-allocations branch April 22, 2026 23:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Linter Area: linter L-CSS Language: CSS and super languages L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants