Skip to content

perf(linter/typescript/array-type): remove full source text clone#23751

Merged
camc314 merged 2 commits into
oxc-project:mainfrom
mds-ant:perf/array-type-full-source-clone-auto
Jun 24, 2026
Merged

perf(linter/typescript/array-type): remove full source text clone#23751
camc314 merged 2 commits into
oxc-project:mainfrom
mds-ant:perf/array-type-full-source-clone-auto

Conversation

@mds-ant

@mds-ant mds-ant commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

What

check_and_report_error_generic in the typescript/array-type rule cloned the entire source file via ctx.source_text().to_string() on every TSArrayType violation, only to use the result as a &str (passed to get_message_type and sliced in the fixer closure). ctx.source_text() already returns &'a str tied to the arena lifetime, so the heap copy is pure waste.

Change

Drop .to_string(); use the borrowed &str directly.

-    let source_text = ctx.source_text().to_string();
+    let source_text = ctx.source_text();
     ...
-    let message_type = get_message_type(type_param, &source_text);
+    let message_type = get_message_type(type_param, source_text);

Impact

Around 1.61 GB of heap allocations saved when running against the microsoft/vscode repo (10,572 files).

Measured by linting with {"typescript/array-type": ["error", {"default": "generic"}]} as the only enabled rule — 40,426 violations across 5,744 files. Release build, glibc system allocator, 3 interleaved runs after warmup, medians reported.

Peak RSS (getrusage ru_maxrss)

baseline this branch Δ (abs) Δ (%)
threads=1 362,236 KB 332,740 KB −29,496 KB −8.1%
threads=64 489,180 KB 459,732 KB −29,448 KB −6.0%

Total heap bytes allocated (LD_PRELOAD malloc/calloc/realloc counter)

baseline this branch Δ (abs) Δ (%)
malloc calls 4,552,293 4,511,867 −40,426 −0.9%
bytes 6,502,015,003 4,893,067,752 −1,608,947,251 −24.7%

The −40,426 calls exactly equals the diagnostic count (one .to_string() per violation).

Wall clock

baseline this branch Δ (abs) Δ (%)
threads=1 4.152 s 4.122 s −0.030 s −0.7%
threads=64 2.695 s 2.588 s −0.107 s −4.0%

Verification

  • cargo test -p oxc_linter --lib array_type passes.

This PR was assisted by Claude Code.

`check_and_report_error_generic` cloned the entire source file via
`ctx.source_text().to_string()` only to pass it as `&str` to
`get_message_type` and slice it in the fixer closure. `ctx.source_text()`
already returns `&'a str`, so the heap copy is unnecessary.
@mds-ant
mds-ant requested a review from camc314 as a code owner June 24, 2026 09:10
@camc314 camc314 changed the title perf(linter): drop full-source clone in array_type rule perf(linter/typescript/array-type): remove full source text clone Jun 24, 2026
@camc314 camc314 self-assigned this Jun 24, 2026
@camc314 camc314 added the A-linter Area - Linter label Jun 24, 2026

@camc314 camc314 left a comment

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.

nice find - thanks!

@camc314
camc314 merged commit 417b506 into oxc-project:main Jun 24, 2026
28 checks passed
@codspeed-hq

codspeed-hq Bot commented Jun 24, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 5 untouched benchmarks
⏩ 66 skipped benchmarks1


Comparing mds-ant:perf/array-type-full-source-clone-auto (6d246fa) with main (e1886a0)

Open in CodSpeed

Footnotes

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

camc314 added a commit that referenced this pull request Jun 29, 2026
# Oxlint
### 💥 BREAKING CHANGES

- 88f4455 str: [**BREAKING**] `Str` and `Ident` methods take
`&GetAllocator` (#23781) (overlookmotel)

### 🚀 Features

- f2091b3 ast: Unify old and new `AstBuilder`s (#23875) (overlookmotel)
- 1c8f50c linter: Add schema for `eslint/no-restricted-import` (#23642)
(Sysix)

### 🐛 Bug Fixes

- 7cb85c4 linter/eslint/no-negated-condition: Add autofix for negated
conditions (#23825) (Yagiz Nizipli)
- f7d1f50 oxlint, oxfmt: Enable `disable_old_builder` Cargo feature for
`oxc_ast` crate (#23886) (overlookmotel)
- d891990 linter/jsx-a11y/role-supports-aria-props: Ignore nullish prop
values (#23865) (Mikhail Baev)
- 94b6599 linter: Deduplicate missing plugin errors (#23853) (camc314)
- eff3eff linter/oxc/branches-sharing-code: Avoid else-if false
positives (#23843) (camc314)
- 2a2d3b9 linter/eslint/prefer-destructuring: Skip
`AssignmentExpression` autofixes (#23818) (camc314)
- ddc24ae linter/eslint/id-length: Respect checkGeneric for mapped type
keys (#23802) (bab)
- cd89202 linter/react/exhaustive-deps: Skip wrapper expression when
analyzing hook initializers (#23793) (camc314)
- 20e8285 linter/unicorn/prefer-native-coercion-function: Allow ts type
predicates (#23774) (camc314)
- d86f60b lsp: Normalize user config path to watch pattern (#23723)
(Sysix)
- 52032cf linter: Newline-terminate tsgolint errors (#23762) (Mikhail
Baev)
- 368fda7 linter/eslint/no-warning-comments: Avoid dropping generated
regex patterns (#23741) (camc314)
- ce44fbd linter/valid-title: Escape disallowed words regex (#23742)
(camc314)
- 3100d11 linter/prefer-called-exactly-once-with: Avoid out-of-bounds
slice panic at end of file (#23625) (Jerry Zhao)
- 742be36 refactor/node/handle-callback-err: Reject invalid regex config
(#23740) (camc314)
- d7be179 linter/eslint/no-restricted-globals: Handle shadowed locals
(#23736) (camc314)
- b3b1ff8 linter/vitest/expect-expect: Handle global vitest detection
correctly (#23734) (camc314)

### ⚡ Performance

- 68f9472 linter/jsx-a11y: Skip lowercasing non-aria attribute names
(#23906) (Lawrence Lin)
- b9312b4 linter/unicorn/prefer-export-from: Use keyed binding lookup
(#23893) (Marius Schulz)
- cd5204e linter/typescript/no-unsafe-declaration-merging: Use keyed
binding lookup (#23894) (Marius Schulz)
- e948498 linter/eslint/prefer-named-capture-group: Only dispatch for
relevant node types (#23868) (Connor Shea)
- 4ac7a8e linter/eslint/max-depth: Derive node types (#23896) (Connor
Shea)
- daeed09 linter/eslint/no-restricted-globals: Only scan unresolved
references (#23890) (camc314)
- e808514 linter/jest-vitest: Speed up no-standalone-expect (#23883)
(camc314)
- 8b165e5 linter/react/exhaustive-deps: Skip non-reactive calls early
(#23882) (camc314)
- 54005e7 linter/eslint/no-unused-vars: Precompute exported bindings
(#23881) (camc314)
- 9bc2f8c linter/unicorn/prefer-number-properties: Speed up global
checks (#23880) (camc314)
- 4ff104f linter: Optimize `require-hook` and `prefer-mock-*` rules to
run on specific node types (#23871) (Connor Shea)
- cc2213b linter: Run `no-underscore-dangle` only when relevant node
types are present (#23867) (Connor Shea)
- 3e55c21 linter/promise/always-return: Narrow to function node types
(#23878) (Connor Shea)
- 7136182 linter/jest-vitest: Speed up no-commented-out-tests (#23864)
(camc314)
- f138264 linter/eslint/no-script-url: Match javascript: prefix without
allocating (#23861) (Lawrence Lin)
- 7ef6895 linter/react/no-array-index-key: Delay index symbol lookup
(#23857) (camc314)
- 26bc171 linter/react/no-array-index-key: Match callback methods
directly (#23856) (camc314)
- 44fbbda linter/jsx-a11y/interactive-supports-focus: Check cheap
conditions first (#23854) (camc314)
- 84a5aa3 linter/eslint/no-extend-native: Skip lowercase references
early (#23851) (camc314)
- 88a74b2 linter/eslint/no-nonoctal-decimal-escape: Scan decimal escapes
as bytes (#23850) (camc314)
- fca69a8 linter: Skip traversal without this expressions (#23845)
(camc314)
- 838fd63 linter: Reduce preallocation for per-file diagnostics `Vec`
(#23705) (Marius Schulz)
- 417b506 linter/typescript/array-type: Remove full source text clone
(#23751) (Marius Schulz)

### 📚 Documentation

- 57e4469 linter/unicorn: Update prefer-dom-node-text-content rationale
(#23933) (Mikhail Baev)
- 3d61dea all: Correct capitalization in comments (#23887)
(overlookmotel)

### 🛡️ Security

- 3cdd18f deps: Update npm packages (#23690) (renovate[bot])
# Oxfmt
### 💥 BREAKING CHANGES

- 259e0cd oxfmt,formatter_graphql: [**BREAKING**] Support draft syntax
with removing prettier fallback (#23326) (leaysgur)
- accbc49 oxfmt: [**BREAKING**] Format `parser:css,less,scss` files +
css-in-js by `oxc_formatter_css` (#23321) (leaysgur)

### 🚀 Features

- dffa4b3 formatter_css: Implement `oxc_formatter_css` (#23320)
(leaysgur)
- 01de9ec oxfmt: Format `parser:graphql` files by
`oxc_formatter_graphql` (#23318) (leaysgur)
- 4e66212 formatter_graphql: Implement oxc_formatter_graphql (#23317)
(leaysgur)

### 🐛 Bug Fixes

- 67325ae formatter_css: Handle frontmatter language (#23819) (leaysgur)
- 3f355e5 formatter_graphql: Improve major prettier diffs (#23419)
(leaysgur)
- 48e2d78 formatter_css: Improve major prettier diffs (#23327)
(leaysgur)
- 8c07cad all: Enable `disable_old_builder` Cargo feature for `oxc_ast`
crate in tests (#23888) (overlookmotel)
- f7d1f50 oxlint, oxfmt: Enable `disable_old_builder` Cargo feature for
`oxc_ast` crate (#23886) (overlookmotel)
- d86f60b lsp: Normalize user config path to watch pattern (#23723)
(Sysix)

### ⚡ Performance

- 4ddcba0 formatter_core: Add printable-ASCII fast path to TextWidth
(#23913) (Lawrence Lin)

### 📚 Documentation

- b4d0dc9 oxfmt,formatter,formatter_css,formatter_core: Update AGENTS.md
(#23814) (leaysgur)

Co-authored-by: Boshen <[email protected]>
Co-authored-by: Cameron <[email protected]>
camc314 added a commit that referenced this pull request Jul 3, 2026
…3751)

## What

`check_and_report_error_generic` in the `typescript/array-type` rule
cloned the entire source file via `ctx.source_text().to_string()` on
every `TSArrayType` violation, only to use the result as a `&str`
(passed to `get_message_type` and sliced in the fixer closure).
`ctx.source_text()` already returns `&'a str` tied to the arena
lifetime, so the heap copy is pure waste.

## Change

Drop `.to_string()`; use the borrowed `&str` directly.

```diff
-    let source_text = ctx.source_text().to_string();
+    let source_text = ctx.source_text();
     ...
-    let message_type = get_message_type(type_param, &source_text);
+    let message_type = get_message_type(type_param, source_text);
```

## Impact

Around **1.61 GB** of heap allocations saved when running against the
`microsoft/vscode` repo (10,572 files).

Measured by linting with `{"typescript/array-type": ["error",
{"default": "generic"}]}` as the only enabled rule — 40,426 violations
across 5,744 files. Release build, glibc system allocator, 3 interleaved
runs after warmup, medians reported.

### Peak RSS (`getrusage` `ru_maxrss`)

|             |   baseline | this branch |    Δ (abs) |  Δ (%) |
|-------------|-----------:|------------:|-----------:|-------:|
| threads=1   | 362,236 KB |  332,740 KB | −29,496 KB |  −8.1% |
| threads=64  | 489,180 KB |  459,732 KB | −29,448 KB |  −6.0% |

### Total heap bytes allocated (`LD_PRELOAD` malloc/calloc/realloc
counter)

| | baseline | this branch | Δ (abs) | Δ (%) |

|--------------|--------------:|--------------:|---------------:|-------:|
| malloc calls | 4,552,293 | 4,511,867 | −40,426 | −0.9% |
| bytes | 6,502,015,003 | 4,893,067,752 | −1,608,947,251 | −24.7% |

The −40,426 calls exactly equals the diagnostic count (one
`.to_string()` per violation).

### Wall clock

|             | baseline | this branch |  Δ (abs) | Δ (%) |
|-------------|---------:|------------:|---------:|------:|
| threads=1   |  4.152 s |     4.122 s | −0.030 s | −0.7% |
| threads=64  |  2.695 s |     2.588 s | −0.107 s | −4.0% |

## Verification

- `cargo test -p oxc_linter --lib array_type` passes.

This PR was assisted by Claude Code.

Co-authored-by: Cameron <[email protected]>
camc314 added a commit that referenced this pull request Jul 3, 2026
# Oxlint
### 💥 BREAKING CHANGES

- 88f4455 str: [**BREAKING**] `Str` and `Ident` methods take
`&GetAllocator` (#23781) (overlookmotel)

### 🚀 Features

- f2091b3 ast: Unify old and new `AstBuilder`s (#23875) (overlookmotel)
- 1c8f50c linter: Add schema for `eslint/no-restricted-import` (#23642)
(Sysix)

### 🐛 Bug Fixes

- 7cb85c4 linter/eslint/no-negated-condition: Add autofix for negated
conditions (#23825) (Yagiz Nizipli)
- f7d1f50 oxlint, oxfmt: Enable `disable_old_builder` Cargo feature for
`oxc_ast` crate (#23886) (overlookmotel)
- d891990 linter/jsx-a11y/role-supports-aria-props: Ignore nullish prop
values (#23865) (Mikhail Baev)
- 94b6599 linter: Deduplicate missing plugin errors (#23853) (camc314)
- eff3eff linter/oxc/branches-sharing-code: Avoid else-if false
positives (#23843) (camc314)
- 2a2d3b9 linter/eslint/prefer-destructuring: Skip
`AssignmentExpression` autofixes (#23818) (camc314)
- ddc24ae linter/eslint/id-length: Respect checkGeneric for mapped type
keys (#23802) (bab)
- cd89202 linter/react/exhaustive-deps: Skip wrapper expression when
analyzing hook initializers (#23793) (camc314)
- 20e8285 linter/unicorn/prefer-native-coercion-function: Allow ts type
predicates (#23774) (camc314)
- d86f60b lsp: Normalize user config path to watch pattern (#23723)
(Sysix)
- 52032cf linter: Newline-terminate tsgolint errors (#23762) (Mikhail
Baev)
- 368fda7 linter/eslint/no-warning-comments: Avoid dropping generated
regex patterns (#23741) (camc314)
- ce44fbd linter/valid-title: Escape disallowed words regex (#23742)
(camc314)
- 3100d11 linter/prefer-called-exactly-once-with: Avoid out-of-bounds
slice panic at end of file (#23625) (Jerry Zhao)
- 742be36 refactor/node/handle-callback-err: Reject invalid regex config
(#23740) (camc314)
- d7be179 linter/eslint/no-restricted-globals: Handle shadowed locals
(#23736) (camc314)
- b3b1ff8 linter/vitest/expect-expect: Handle global vitest detection
correctly (#23734) (camc314)

### ⚡ Performance

- 68f9472 linter/jsx-a11y: Skip lowercasing non-aria attribute names
(#23906) (Lawrence Lin)
- b9312b4 linter/unicorn/prefer-export-from: Use keyed binding lookup
(#23893) (Marius Schulz)
- cd5204e linter/typescript/no-unsafe-declaration-merging: Use keyed
binding lookup (#23894) (Marius Schulz)
- e948498 linter/eslint/prefer-named-capture-group: Only dispatch for
relevant node types (#23868) (Connor Shea)
- 4ac7a8e linter/eslint/max-depth: Derive node types (#23896) (Connor
Shea)
- daeed09 linter/eslint/no-restricted-globals: Only scan unresolved
references (#23890) (camc314)
- e808514 linter/jest-vitest: Speed up no-standalone-expect (#23883)
(camc314)
- 8b165e5 linter/react/exhaustive-deps: Skip non-reactive calls early
(#23882) (camc314)
- 54005e7 linter/eslint/no-unused-vars: Precompute exported bindings
(#23881) (camc314)
- 9bc2f8c linter/unicorn/prefer-number-properties: Speed up global
checks (#23880) (camc314)
- 4ff104f linter: Optimize `require-hook` and `prefer-mock-*` rules to
run on specific node types (#23871) (Connor Shea)
- cc2213b linter: Run `no-underscore-dangle` only when relevant node
types are present (#23867) (Connor Shea)
- 3e55c21 linter/promise/always-return: Narrow to function node types
(#23878) (Connor Shea)
- 7136182 linter/jest-vitest: Speed up no-commented-out-tests (#23864)
(camc314)
- f138264 linter/eslint/no-script-url: Match javascript: prefix without
allocating (#23861) (Lawrence Lin)
- 7ef6895 linter/react/no-array-index-key: Delay index symbol lookup
(#23857) (camc314)
- 26bc171 linter/react/no-array-index-key: Match callback methods
directly (#23856) (camc314)
- 44fbbda linter/jsx-a11y/interactive-supports-focus: Check cheap
conditions first (#23854) (camc314)
- 84a5aa3 linter/eslint/no-extend-native: Skip lowercase references
early (#23851) (camc314)
- 88a74b2 linter/eslint/no-nonoctal-decimal-escape: Scan decimal escapes
as bytes (#23850) (camc314)
- fca69a8 linter: Skip traversal without this expressions (#23845)
(camc314)
- 838fd63 linter: Reduce preallocation for per-file diagnostics `Vec`
(#23705) (Marius Schulz)
- 417b506 linter/typescript/array-type: Remove full source text clone
(#23751) (Marius Schulz)

### 📚 Documentation

- 57e4469 linter/unicorn: Update prefer-dom-node-text-content rationale
(#23933) (Mikhail Baev)
- 3d61dea all: Correct capitalization in comments (#23887)
(overlookmotel)

### 🛡️ Security

- 3cdd18f deps: Update npm packages (#23690) (renovate[bot])
# Oxfmt
### 💥 BREAKING CHANGES

- 259e0cd oxfmt,formatter_graphql: [**BREAKING**] Support draft syntax
with removing prettier fallback (#23326) (leaysgur)
- accbc49 oxfmt: [**BREAKING**] Format `parser:css,less,scss` files +
css-in-js by `oxc_formatter_css` (#23321) (leaysgur)

### 🚀 Features

- dffa4b3 formatter_css: Implement `oxc_formatter_css` (#23320)
(leaysgur)
- 01de9ec oxfmt: Format `parser:graphql` files by
`oxc_formatter_graphql` (#23318) (leaysgur)
- 4e66212 formatter_graphql: Implement oxc_formatter_graphql (#23317)
(leaysgur)

### 🐛 Bug Fixes

- 67325ae formatter_css: Handle frontmatter language (#23819) (leaysgur)
- 3f355e5 formatter_graphql: Improve major prettier diffs (#23419)
(leaysgur)
- 48e2d78 formatter_css: Improve major prettier diffs (#23327)
(leaysgur)
- 8c07cad all: Enable `disable_old_builder` Cargo feature for `oxc_ast`
crate in tests (#23888) (overlookmotel)
- f7d1f50 oxlint, oxfmt: Enable `disable_old_builder` Cargo feature for
`oxc_ast` crate (#23886) (overlookmotel)
- d86f60b lsp: Normalize user config path to watch pattern (#23723)
(Sysix)

### ⚡ Performance

- 4ddcba0 formatter_core: Add printable-ASCII fast path to TextWidth
(#23913) (Lawrence Lin)

### 📚 Documentation

- b4d0dc9 oxfmt,formatter,formatter_css,formatter_core: Update AGENTS.md
(#23814) (leaysgur)

Co-authored-by: Boshen <[email protected]>
Co-authored-by: Cameron <[email protected]>
camc314 added a commit that referenced this pull request Jul 11, 2026
…r comment (#24369)

## What

`get_last_comment_line` in the `typescript/prefer-ts-expect-error` rule
heap-allocated a
`String` (`String::from(raw)` / `String::from(raw.lines().last()...)`)
for **every comment in the
file**. It is called unconditionally from `is_valid_ts_ignore_present`,
which `run_once` invokes on
every comment *before* any `@ts-ignore` match — so the allocation
happens once per comment even in
files with zero violations. The returned value is only ever read as
`&str`
(`test_single_line_comment` / `test_multi_line_comment`), never mutated
or stored. `raw` is
`ctx.source_range(...)` → `&'a str` tied to the arena, and
`str::lines().last()` yields a borrowed
slice of it, so the heap copy is pure waste.

## Change

Return a borrowed `&'a str` instead of an owned `String`:

```diff
-fn get_last_comment_line(comment: Comment, raw: &str) -> String {
+fn get_last_comment_line<'a>(comment: Comment, raw: &'a str) -> &'a str {
     if comment.is_line() {
-        return String::from(raw);
+        return raw;
     }
-
-    String::from(raw.lines().last().unwrap_or(raw))
+    raw.lines().last().unwrap_or(raw)
 }
```

(The caller's `test_*` calls take `&str`, so `&line` becomes `line`.)

## Impact

One heap allocation removed per comment. Measured on a synthetic
11k-line TS file with 4,000
comments (`typescript/prefer-ts-expect-error` as the only enabled rule),
counting system-allocator
activity during `Linter::run` only:

| metric      |   baseline |  this branch |        Δ |
|-------------|-----------:|-------------:|---------:|
| allocs/iter |     12,006 |        8,006 | **−4,000 (−33%)** |
| bytes/iter  |  1,479,224 |    1,377,057 | −102,167 |

The −4,000 allocs/iter exactly equals the comment count (one
`String::from` per comment).

### Wall clock

This is a memory/allocation optimization, not a speed one — same class
as #23751 (whose own
wall-clock delta was only −0.7% to −4%). In an isolated single-rule
Criterion benchmark
(release build, `tasks/benchmark`, the repo's deterministic
`NeverGrowInPlace` allocator),
the change trends slightly faster (~−4% median over 40 paired runs,
fixed faster in 57%), but the effect is small and near the noise floor
of a shared
machine, so no end-to-end speedup should be assumed. The reliable,
deterministic win is the
reduced allocation count above (production uses mimalloc, where these
small allocations are
cheap).

## Verification

- `cargo test -p oxc_linter --lib prefer_ts_expect_error` passes.

This PR was assisted by Claude Code.

---------

Co-authored-by: Cameron <[email protected]>
camc314 added a commit that referenced this pull request Jul 11, 2026
… per violation (#24370)

## What

When `typescript/consistent-type-imports` reports an import whose
specifiers are only used as
types, `run` built **two** `Vec`s per violation:

```rust
let type_names = type_references_without_type_qualifier
    .iter()
    .map(|specifier| specifier.name())          // Vec<Cow<'a, str>>
    .collect::<Vec<_>>();

let type_imports = format_word_list(&type_names);
let type_names =
    type_names.iter().map(std::convert::AsRef::as_ref).collect::<Vec<_>>();  // Vec<&str>
```

`ImportDeclarationSpecifier::name()` always returns
`Cow::Borrowed(self.local().name.as_str())`, so
the first `Vec<Cow>` is never anything but borrowed data, and the second
`collect` allocates an
entire additional `Vec` on every violation just to convert `Cow → &str`
for the fixer. Both the
message builder (`format_word_list`) and the fixer only need `&str`.

## Change

Collect a single `Vec<&str>` directly from the arena-backed names and
drop the second `collect`;
`format_word_list` takes `&[&str]`:

```diff
     let type_names = type_references_without_type_qualifier
         .iter()
-        .map(|specifier| specifier.name())
+        .map(|specifier| specifier.local().name.as_str())
         .collect::<Vec<_>>();

     let type_imports = format_word_list(&type_names);
-    let type_names =
-        type_names.iter().map(std::convert::AsRef::as_ref).collect::<Vec<_>>();
```
```diff
-fn format_word_list<'a>(words: &[Cow<'a, str>]) -> Cow<'a, str> {
+fn format_word_list<'a>(words: &[&'a str]) -> Cow<'a, str> {
     match words.len() {
         0 => Cow::Borrowed(""),
-        1 => words[0].clone(),
+        1 => Cow::Borrowed(words[0]),
         2 => Cow::Owned(format!("{} and {}", words[0], words[1])),
```

## Impact

One `Vec` allocation removed per reported import, and the remaining
`Vec` shrinks from
`Cow<str>` (24 B/elem) to `&str` (16 B/elem). Measured on a synthetic
9k-line TS file with 3,000
type-only import declarations (`typescript/consistent-type-imports` as
the only enabled rule),
counting system-allocator activity during `Linter::run` only:

| metric      |   baseline |  this branch |        Δ |
|-------------|-----------:|-------------:|---------:|
| allocs/iter |     21,006 |       18,006 | **−3,000 (−14%)** |
| bytes/iter  |  2,555,972 |    2,339,972 | −216,000 |

The −3,000 allocs/iter exactly equals the number of reported imports
(one redundant `Vec` each).

### Wall clock

This is a memory/allocation optimization, not a speed one — same class
as #23751 (whose own
wall-clock delta was only −0.7% to −4%). In an isolated single-rule
Criterion benchmark
(release build, `tasks/benchmark`, the repo's deterministic
`NeverGrowInPlace` allocator),
the change trends slightly faster (~−5% median over 40 paired runs,
fixed faster in 57%), but the effect is small and near the noise floor
of a shared
machine, so no end-to-end speedup should be assumed. The reliable,
deterministic win is the
reduced allocation count above (production uses mimalloc, where these
small allocations are
cheap).

## Verification

- `cargo test -p oxc_linter --lib consistent_type_imports` passes.

This PR was assisted by Claude Code.

Co-authored-by: Cameron <[email protected]>
camc314 added a commit that referenced this pull request Jul 11, 2026
…tifier (#24371)

## What

`NoUnderscoreDangle::is_allowed` heap-allocated a `String`
(`name.to_string()`) on every call, only
to feed `Vec::contains` for a membership test:

```rust
fn is_allowed(&self, name: &str) -> bool {
    is_always_allowed(name) || self.allow.contains(&name.to_string())
}
```

`is_allowed` runs from `report`, which fires for every checked
identifier (bindings, members,
private fields, function/method/property names). `self.allow` is a
`Vec<String>` and `name` is
already `&str`, so the `to_string()` allocates a throwaway `String`
purely to compare it, then drops
it — once per identifier the rule inspects.

## Change

Compare against the existing `String`s by reference; no allocation:

```diff
-    is_always_allowed(name) || self.allow.contains(&name.to_string())
+    is_always_allowed(name) || self.allow.iter().any(|allowed| allowed == name)
```

## Impact

One heap allocation removed per checked identifier. Measured on a
synthetic 8k-line TS file with
8,000 dangling-underscore identifiers (`eslint/no-underscore-dangle` as
the only enabled rule),
counting system-allocator activity during `Linter::run` only:

| metric      |   baseline |  this branch |        Δ |
|-------------|-----------:|-------------:|---------:|
| allocs/iter |     32,006 |       24,006 | **−8,000 (−25%)** |
| bytes/iter  |  5,236,309 |    5,174,529 |  −61,780 |

The −8,000 allocs/iter exactly equals the number of identifiers checked
(one `to_string` each).

### Wall clock

This is a memory/allocation optimization, not a speed one — same class
as #23751 (whose own
wall-clock delta was only −0.7% to −4%). In an isolated single-rule
Criterion benchmark
(release build, `tasks/benchmark`, the repo's deterministic
`NeverGrowInPlace` allocator),
the change trends flat (fixed faster in only 20/40 paired runs — no
measurable wall-clock change; the removed `String` is dwarfed by the
rule's unavoidable per-diagnostic message allocations), but the effect
is small and near the noise floor of a shared
machine, so no end-to-end speedup should be assumed. The reliable,
deterministic win is the
reduced allocation count above (production uses mimalloc, where these
small allocations are
cheap).

## Verification

- `cargo test -p oxc_linter --lib no_underscore_dangle` passes.

This PR was assisted by Claude Code.

Co-authored-by: Cameron <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-linter Area - Linter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants