Conversation
|
312b6db to
4027bb2
Compare
This stack of pull requests is managed by Graphite. Learn more about stacking. |
4027bb2 to
5ef8c9a
Compare
5ef8c9a to
5f731a2
Compare
cb3acb7 to
0cabd62
Compare
0cabd62 to
c0d4b0c
Compare
WalkthroughAdds a vendored .gitattributes entry and introduces filesystem-driven benchmark fixtures for the HTML parser and formatter. Seventeen new synthetic fixture files (Astro, Svelte, Vue, and numerous HTML edge-case pages) were added under crates/biome_html_parser/benches/fixtures. Bench code in both parser and formatter now loads fixtures recursively at runtime, skipping Markdown, and runs uncached and cached benchmarks per fixture with diagnostic reporting. Global allocator selection blocks were added to the formatter bench. A dev-dependency on biome_string_case was added for fixture handling. Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
crates/biome_html_formatter/benches/html_formatter.rs (1)
8-62: Consider sorting fixtures for deterministic benchmark order.
read_dirorder varies by platform, so a sort keeps IDs stable across runs.♻️ Suggested tweak
visit(&fixtures_root, &fixtures_root, &mut cases); + cases.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1))); cases🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/biome_html_formatter/benches/html_formatter.rs` around lines 8 - 62, The benchmark fixture loading is non-deterministic because fs::read_dir iteration order varies; in load_fixtures (and its nested visit function) sort directory entries before processing and sort the final cases vector to ensure stable ordering: after collecting entries in visit sort by path/file name (or collect into a Vec, sort by path.display() or (group,name)), and before returning from load_fixtures sort cases (e.g., by group then name) so benchmark IDs remain stable across runs.crates/biome_html_parser/benches/html_parser.rs (1)
133-149: Move diagnostics collection out of the timed loop.
Collecting diagnostics insideb.iterskews timings and can balloon if any fixture emits diagnostics. Pre‑parse once and log once, then benchmark clean parses.♻️ Suggested refactor
- let mut diagnostics = vec![]; group.throughput(Throughput::Bytes(code.len() as u64)); @@ - group.bench_with_input(BenchmarkId::new(&id, "uncached"), &code, |b, _| { - b.iter(|| { - let result = black_box(parse_html(code, HtmlParseOptions::from(&file_source))); - diagnostics.extend(result.into_diagnostics()); - }) - }); - - for diagnostic in diagnostics { - let diagnostic = diagnostic.with_file_source_code(code).with_file_path(&id); - println!("{}", print_diagnostic_to_string(&diagnostic)); - } + let diagnostics = parse_html(code, HtmlParseOptions::from(&file_source)).into_diagnostics(); + for diagnostic in diagnostics { + let diagnostic = diagnostic.with_file_source_code(code).with_file_path(&id); + println!("{}", print_diagnostic_to_string(&diagnostic)); + } + group.bench_with_input(BenchmarkId::new(&id, "uncached"), &code, |b, _| { + b.iter(|| { + black_box(parse_html(code, HtmlParseOptions::from(&file_source))); + }) + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/biome_html_parser/benches/html_parser.rs` around lines 133 - 149, The benchmark currently extends diagnostics inside the timed closure (in the b.iter |b, _| { ... } block) which skews measurements and can grow unbounded; instead, call parse_html once before group.bench_with_input to collect and store diagnostics (using HtmlFileSource::try_from_extension, HtmlParseOptions::from, parse_html and result.into_diagnostics()), then use group.bench_with_input's b.iter to repeatedly call parse_html for timing only and discard or black_box the parse result without extending the diagnostics vec; ensure the pre-parse uses the same code and options so the timed loop measures clean parses only.
🤖 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_html_parser/benches/fixtures/synthetic/svelte-directives.svelte`:
- Around line 1-156: The template references undeclared symbols actionParam,
actionConfig, and handleLongPress; add declarations to the <script> block:
declare sensible default values for actionParam and actionConfig (e.g., let
actionParam = null; let actionConfig = {}; or appropriate types used by
action2/action3) and implement a handleLongPress function (e.g., function
handleLongPress(event, params) { /* noop or set state like longpress handling */
}) so that actionParam, actionConfig, and handleLongPress are defined and used
by use:action2, use:action3, and use:longpress respectively; ensure names match
exactly as referenced in the template.
In `@crates/biome_html_parser/benches/fixtures/synthetic/vue-dynamic-args.vue`:
- Around line 39-167: There is a duplicate const declaration for clickEvent;
rename the second script variable (the one used for the table) to a unique name
(e.g., tableClickEvent) and update the template binding that uses it (the table
element with @[clickEvent].stop="onTableClick") to use the new name
@[tableClickEvent]. Ensure you change the declaration (const clickEvent =
ref('click') → const tableClickEvent = ref('click')) and any template references
that pointed to the second clickEvent.
---
Nitpick comments:
In `@crates/biome_html_formatter/benches/html_formatter.rs`:
- Around line 8-62: The benchmark fixture loading is non-deterministic because
fs::read_dir iteration order varies; in load_fixtures (and its nested visit
function) sort directory entries before processing and sort the final cases
vector to ensure stable ordering: after collecting entries in visit sort by
path/file name (or collect into a Vec, sort by path.display() or (group,name)),
and before returning from load_fixtures sort cases (e.g., by group then name) so
benchmark IDs remain stable across runs.
In `@crates/biome_html_parser/benches/html_parser.rs`:
- Around line 133-149: The benchmark currently extends diagnostics inside the
timed closure (in the b.iter |b, _| { ... } block) which skews measurements and
can grow unbounded; instead, call parse_html once before group.bench_with_input
to collect and store diagnostics (using HtmlFileSource::try_from_extension,
HtmlParseOptions::from, parse_html and result.into_diagnostics()), then use
group.bench_with_input's b.iter to repeatedly call parse_html for timing only
and discard or black_box the parse result without extending the diagnostics vec;
ensure the pre-parse uses the same code and options so the timed loop measures
clean parses only.
| <script> | ||
| let value = ''; | ||
| let checked = false; | ||
| let selected = ''; | ||
| let files = null; | ||
| let group = []; | ||
| let innerWidth = 0; | ||
| let innerHeight = 0; | ||
| let online = true; | ||
| let visible = true; | ||
| let fullscreenElement = null; | ||
| let className = 'default'; | ||
| let color = '#ff0000'; | ||
| let fontSize = '16px'; | ||
| let opacity = 1; | ||
| let transform = 'none'; | ||
| let mounted = false; | ||
| let hovered = false; | ||
| let focused = false; | ||
| let validated = false; | ||
| let inputRef; | ||
| let selectRef; | ||
| let boxRef; | ||
| let boxWidth; | ||
| let boxHeight; | ||
| let offsetWidth; | ||
| let offsetHeight; | ||
| let scrollY; | ||
| let isIndeterminate = false; | ||
| let currentTime = 0; | ||
| let duration = 0; | ||
| let paused = true; | ||
| let volume = 1; | ||
| let muted = false; | ||
| let playbackRate = 1; | ||
| let active = false; | ||
| let items = [ | ||
| { id: 1, name: 'A', visible: false }, | ||
| { id: 2, name: 'B', visible: false }, | ||
| { id: 3, name: 'C', visible: false }, | ||
| ]; | ||
|
|
||
| function handleInput() {} | ||
| function handleChange() {} | ||
| function handleKeyPress() {} | ||
| function handleFirstKey() {} | ||
| function handleKeyUp() {} | ||
| function action1(node) {} | ||
| function action2(node, param) {} | ||
| function action3(node, config) {} | ||
| function toggleCheck() {} | ||
| function handleSelect() {} | ||
| function handleFileSelect() {} | ||
| function handleGroupChange() {} | ||
| function fade(node, params) {} | ||
| function fly(node, params) {} | ||
| function slide(node, params) {} | ||
| function flip(node, params) {} | ||
| function tooltip(node, params) {} | ||
| function clickOutside(node, handler) {} | ||
| function longpress(node, params) {} | ||
| function handleBoxClick() {} | ||
| function handleDoubleClick() {} | ||
| function scale(node, params) {} | ||
| function blur(node, params) {} | ||
| function inView(node, params) {} | ||
| function videoAction(node) {} | ||
| function audioEnhancer(node) {} | ||
| function handleTimeUpdate() {} | ||
| function handleAudioPlay() {} | ||
| function handleAudioPause() {} | ||
| </script> | ||
|
|
||
| <svelte:window bind:innerWidth bind:innerHeight bind:online /> | ||
| <svelte:document bind:visibilityState={visible} bind:fullscreenElement /> | ||
| <svelte:body bind:scrollY /> | ||
|
|
||
| <div class="directives-container"> | ||
| <input | ||
| type="text" | ||
| bind:value | ||
| bind:this={inputRef} | ||
| class:active={focused} | ||
| class:invalid={!validated} | ||
| style:color | ||
| style:font-size={fontSize} | ||
| style:opacity={`${opacity}%`} | ||
| style:transform | ||
| on:input={handleInput} | ||
| on:change={handleChange} | ||
| on:focus={() => focused = true} | ||
| on:blur={() => focused = false} | ||
| on:keypress|preventDefault|stopPropagation={handleKeyPress} | ||
| on:keydown|once={handleFirstKey} | ||
| on:keyup|self={handleKeyUp} | ||
| use:action1 | ||
| use:action2={actionParam} | ||
| use:action3|param1|param2={actionConfig} | ||
| /> | ||
|
|
||
| <input | ||
| type="checkbox" | ||
| bind:checked | ||
| bind:indeterminate={isIndeterminate} | ||
| on:click|preventDefault={toggleCheck} | ||
| class:checked | ||
| class:indeterminate={isIndeterminate} | ||
| /> | ||
|
|
||
| <select | ||
| bind:value={selected} | ||
| bind:this={selectRef} | ||
| on:change|stopPropagation={handleSelect} | ||
| class:has-value={selected !== ''} | ||
| > | ||
| <option value="">Select...</option> | ||
| <option value="a">Option A</option> | ||
| <option value="b">Option B</option> | ||
| </select> | ||
|
|
||
| <input | ||
| type="file" | ||
| bind:files | ||
| multiple | ||
| accept="image/*" | ||
| on:change|preventDefault={handleFileSelect} | ||
| /> | ||
|
|
||
| {#each ['a', 'b', 'c', 'd', 'e'] as option} | ||
| <label> | ||
| <input | ||
| type="checkbox" | ||
| bind:group | ||
| value={option} | ||
| on:change|stopPropagation={handleGroupChange} | ||
| /> | ||
| <span class:selected={group.includes(option)}>{option}</span> | ||
| </label> | ||
| {/each} | ||
|
|
||
| <div | ||
| class="animated-box" | ||
| class:mounted | ||
| class:hovered | ||
| class:visible | ||
| style:background-color={color} | ||
| style:border-color={checked ? 'green' : 'red'} | ||
| style:display={visible ? 'block' : 'none'} | ||
| in:fade={{ duration: 300 }} | ||
| out:fly={{ y: 20, duration: 200 }} | ||
| transition:slide|local={{ duration: 400 }} | ||
| animate:flip={{ duration: 250 }} | ||
| use:tooltip={{ text: 'This is a tooltip' }} | ||
| use:clickOutside={() => (active = false)} | ||
| use:longpress|preventDefault={handleLongPress} | ||
| on:mouseenter={() => hovered = true} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check for definitions and references of the three identifiers
echo "=== Searching for actionParam, actionConfig, handleLongPress ==="
rg -n 'actionParam|actionConfig|handleLongPress' crates/biome_html_parser/benches/fixtures/synthetic/svelte-directives.svelteRepository: biomejs/biome
Length of output: 257
Add the missing variable and function declarations in the script block.
actionParam, actionConfig, and handleLongPress are referenced in the template but not defined in the script section. Even though this is a synthetic benchmark fixture, keeping it clean and compilable makes it a better example.
Suggested fix
let items = [
{ id: 1, name: 'A', visible: false },
{ id: 2, name: 'B', visible: false },
{ id: 3, name: 'C', visible: false },
];
+ let actionParam = null;
+ let actionConfig = {};
function longpress(node, params) {}
+ function handleLongPress() {}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <script> | |
| let value = ''; | |
| let checked = false; | |
| let selected = ''; | |
| let files = null; | |
| let group = []; | |
| let innerWidth = 0; | |
| let innerHeight = 0; | |
| let online = true; | |
| let visible = true; | |
| let fullscreenElement = null; | |
| let className = 'default'; | |
| let color = '#ff0000'; | |
| let fontSize = '16px'; | |
| let opacity = 1; | |
| let transform = 'none'; | |
| let mounted = false; | |
| let hovered = false; | |
| let focused = false; | |
| let validated = false; | |
| let inputRef; | |
| let selectRef; | |
| let boxRef; | |
| let boxWidth; | |
| let boxHeight; | |
| let offsetWidth; | |
| let offsetHeight; | |
| let scrollY; | |
| let isIndeterminate = false; | |
| let currentTime = 0; | |
| let duration = 0; | |
| let paused = true; | |
| let volume = 1; | |
| let muted = false; | |
| let playbackRate = 1; | |
| let active = false; | |
| let items = [ | |
| { id: 1, name: 'A', visible: false }, | |
| { id: 2, name: 'B', visible: false }, | |
| { id: 3, name: 'C', visible: false }, | |
| ]; | |
| function handleInput() {} | |
| function handleChange() {} | |
| function handleKeyPress() {} | |
| function handleFirstKey() {} | |
| function handleKeyUp() {} | |
| function action1(node) {} | |
| function action2(node, param) {} | |
| function action3(node, config) {} | |
| function toggleCheck() {} | |
| function handleSelect() {} | |
| function handleFileSelect() {} | |
| function handleGroupChange() {} | |
| function fade(node, params) {} | |
| function fly(node, params) {} | |
| function slide(node, params) {} | |
| function flip(node, params) {} | |
| function tooltip(node, params) {} | |
| function clickOutside(node, handler) {} | |
| function longpress(node, params) {} | |
| function handleBoxClick() {} | |
| function handleDoubleClick() {} | |
| function scale(node, params) {} | |
| function blur(node, params) {} | |
| function inView(node, params) {} | |
| function videoAction(node) {} | |
| function audioEnhancer(node) {} | |
| function handleTimeUpdate() {} | |
| function handleAudioPlay() {} | |
| function handleAudioPause() {} | |
| </script> | |
| <svelte:window bind:innerWidth bind:innerHeight bind:online /> | |
| <svelte:document bind:visibilityState={visible} bind:fullscreenElement /> | |
| <svelte:body bind:scrollY /> | |
| <div class="directives-container"> | |
| <input | |
| type="text" | |
| bind:value | |
| bind:this={inputRef} | |
| class:active={focused} | |
| class:invalid={!validated} | |
| style:color | |
| style:font-size={fontSize} | |
| style:opacity={`${opacity}%`} | |
| style:transform | |
| on:input={handleInput} | |
| on:change={handleChange} | |
| on:focus={() => focused = true} | |
| on:blur={() => focused = false} | |
| on:keypress|preventDefault|stopPropagation={handleKeyPress} | |
| on:keydown|once={handleFirstKey} | |
| on:keyup|self={handleKeyUp} | |
| use:action1 | |
| use:action2={actionParam} | |
| use:action3|param1|param2={actionConfig} | |
| /> | |
| <input | |
| type="checkbox" | |
| bind:checked | |
| bind:indeterminate={isIndeterminate} | |
| on:click|preventDefault={toggleCheck} | |
| class:checked | |
| class:indeterminate={isIndeterminate} | |
| /> | |
| <select | |
| bind:value={selected} | |
| bind:this={selectRef} | |
| on:change|stopPropagation={handleSelect} | |
| class:has-value={selected !== ''} | |
| > | |
| <option value="">Select...</option> | |
| <option value="a">Option A</option> | |
| <option value="b">Option B</option> | |
| </select> | |
| <input | |
| type="file" | |
| bind:files | |
| multiple | |
| accept="image/*" | |
| on:change|preventDefault={handleFileSelect} | |
| /> | |
| {#each ['a', 'b', 'c', 'd', 'e'] as option} | |
| <label> | |
| <input | |
| type="checkbox" | |
| bind:group | |
| value={option} | |
| on:change|stopPropagation={handleGroupChange} | |
| /> | |
| <span class:selected={group.includes(option)}>{option}</span> | |
| </label> | |
| {/each} | |
| <div | |
| class="animated-box" | |
| class:mounted | |
| class:hovered | |
| class:visible | |
| style:background-color={color} | |
| style:border-color={checked ? 'green' : 'red'} | |
| style:display={visible ? 'block' : 'none'} | |
| in:fade={{ duration: 300 }} | |
| out:fly={{ y: 20, duration: 200 }} | |
| transition:slide|local={{ duration: 400 }} | |
| animate:flip={{ duration: 250 }} | |
| use:tooltip={{ text: 'This is a tooltip' }} | |
| use:clickOutside={() => (active = false)} | |
| use:longpress|preventDefault={handleLongPress} | |
| on:mouseenter={() => hovered = true} | |
| <script> | |
| let value = ''; | |
| let checked = false; | |
| let selected = ''; | |
| let files = null; | |
| let group = []; | |
| let innerWidth = 0; | |
| let innerHeight = 0; | |
| let online = true; | |
| let visible = true; | |
| let fullscreenElement = null; | |
| let className = 'default'; | |
| let color = '#ff0000'; | |
| let fontSize = '16px'; | |
| let opacity = 1; | |
| let transform = 'none'; | |
| let mounted = false; | |
| let hovered = false; | |
| let focused = false; | |
| let validated = false; | |
| let inputRef; | |
| let selectRef; | |
| let boxRef; | |
| let boxWidth; | |
| let boxHeight; | |
| let offsetWidth; | |
| let offsetHeight; | |
| let scrollY; | |
| let isIndeterminate = false; | |
| let currentTime = 0; | |
| let duration = 0; | |
| let paused = true; | |
| let volume = 1; | |
| let muted = false; | |
| let playbackRate = 1; | |
| let active = false; | |
| let items = [ | |
| { id: 1, name: 'A', visible: false }, | |
| { id: 2, name: 'B', visible: false }, | |
| { id: 3, name: 'C', visible: false }, | |
| ]; | |
| let actionParam = null; | |
| let actionConfig = {}; | |
| function handleInput() {} | |
| function handleChange() {} | |
| function handleKeyPress() {} | |
| function handleFirstKey() {} | |
| function handleKeyUp() {} | |
| function action1(node) {} | |
| function action2(node, param) {} | |
| function action3(node, config) {} | |
| function toggleCheck() {} | |
| function handleSelect() {} | |
| function handleFileSelect() {} | |
| function handleGroupChange() {} | |
| function fade(node, params) {} | |
| function fly(node, params) {} | |
| function slide(node, params) {} | |
| function flip(node, params) {} | |
| function tooltip(node, params) {} | |
| function clickOutside(node, handler) {} | |
| function longpress(node, params) {} | |
| function handleLongPress() {} | |
| function handleBoxClick() {} | |
| function handleDoubleClick() {} | |
| function scale(node, params) {} | |
| function blur(node, params) {} | |
| function inView(node, params) {} | |
| function videoAction(node) {} | |
| function audioEnhancer(node) {} | |
| function handleTimeUpdate() {} | |
| function handleAudioPlay() {} | |
| function handleAudioPause() {} | |
| </script> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/biome_html_parser/benches/fixtures/synthetic/svelte-directives.svelte`
around lines 1 - 156, The template references undeclared symbols actionParam,
actionConfig, and handleLongPress; add declarations to the <script> block:
declare sensible default values for actionParam and actionConfig (e.g., let
actionParam = null; let actionConfig = {}; or appropriate types used by
action2/action3) and implement a handleLongPress function (e.g., function
handleLongPress(event, params) { /* noop or set state like longpress handling */
}) so that actionParam, actionConfig, and handleLongPress are defined and used
by use:action2, use:action3, and use:longpress respectively; ensure names match
exactly as referenced in the template.
| <table @[clickEvent].stop="onTableClick"> | ||
| <tr v-for="row in rows" :key="row.id" | ||
| :[row.attr]="row.value" | ||
| @[row.event].prevent="row.handler" | ||
| @[row.event2].capture.once="row.handler2"> | ||
| <td v-for="cell in row.cells" :key="cell.id" | ||
| :[cell.prop]="cell.value" | ||
| @[cell.event].stop.prevent="cell.handler"> | ||
| {{ cell.content }} | ||
| </td> | ||
| </tr> | ||
| </table> | ||
|
|
||
| <nav @[navEvent].prevent="onNav"> | ||
| <a v-for="link in links" :key="link.id" | ||
| :[link.href]="link.url" | ||
| :[link.target]="link.targetVal" | ||
| @[link.click].prevent.stop="link.onClick" | ||
| @[link.mouseenter].once="link.onEnter"> | ||
| {{ link.text }} | ||
| </a> | ||
| </nav> | ||
|
|
||
| <modal :[modalProp]="modalValue" | ||
| @[modalShow]="onShow" | ||
| @[modalHide].once="onHide" | ||
| @[modalClose].prevent.stop="onClose"> | ||
| <header :[headerProp]="headerValue" @[headerEvent]="onHeaderEvent"> | ||
| <h2>{{ modalTitle }}</h2> | ||
| </header> | ||
| <section :[contentProp]="contentValue" @[contentEvent].capture="onContentEvent"> | ||
| <p>{{ modalContent }}</p> | ||
| </section> | ||
| <footer :[footerProp]="footerValue" @[footerEvent].stop="onFooterEvent"> | ||
| <button :[actionProp]="actionValue" @[actionEvent].prevent="onAction">Close</button> | ||
| </footer> | ||
| </modal> | ||
|
|
||
| <card v-for="card in cards" :key="card.id" | ||
| :[card.titleProp]="card.title" | ||
| :[card.imageProp]="card.image" | ||
| :[card.classProp]="card.class" | ||
| @[card.click].prevent.stop.once="card.onClick" | ||
| @[card.hover].capture="card.onHover"> | ||
| <template #[card.slotName]> | ||
| {{ card.slotContent }} | ||
| </template> | ||
| </card> | ||
|
|
||
| <tabs :[activeTabProp]="activeTab" @[tabChange]="onTabChange"> | ||
| <tab v-for="tab in tabs" :key="tab.id" | ||
| :[tab.nameProp]="tab.name" | ||
| :[tab.disabledProp]="tab.disabled" | ||
| @[tab.select].prevent="tab.onSelect" | ||
| @[tab.deselect].once="tab.onDeselect"> | ||
| {{ tab.content }} | ||
| </tab> | ||
| </tabs> | ||
|
|
||
| <dropdown :[visibleProp]="dropdownVisible" | ||
| @[toggleEvent].prevent="toggleDropdown" | ||
| @[selectEvent].stop="onSelect" | ||
| @[outsideEvent].capture.once="onOutsideClick"> | ||
| <item v-for="item in dropdownItems" :key="item.id" | ||
| :[item.valueProp]="item.value" | ||
| :[item.disabledProp]="item.disabled" | ||
| @[item.click].prevent.stop="item.onClick" | ||
| @[item.hover]="item.onHover"> | ||
| {{ item.label }} | ||
| </item> | ||
| </dropdown> | ||
|
|
||
| <tooltip :[tooltipProp]="tooltipContent" | ||
| :[positionProp]="tooltipPosition" | ||
| @[showEvent].once="onTooltipShow" | ||
| @[hideEvent].prevent="onTooltipHide"> | ||
| <span @[triggerEvent]="onTrigger">Hover me</span> | ||
| </tooltip> | ||
| </div> | ||
| </template> | ||
|
|
||
| <script setup> | ||
| import { ref } from 'vue' | ||
|
|
||
| const dynamicProp = ref('value') | ||
| const dynamicEvent = ref('input') | ||
| const value = ref('test') | ||
| const handler = () => {} | ||
|
|
||
| const typeProp = ref('type') | ||
| const buttonType = ref('button') | ||
| const clickEvent = ref('click') | ||
| const hoverEvent = ref('mouseenter') | ||
| const buttonText = ref('Click me') | ||
| const onClick = () => {} | ||
| const onHover = () => {} | ||
|
|
||
| const classProp = ref('class') | ||
| const styleProp = ref('style') | ||
| const dynamicClass = ref('dynamic') | ||
| const dynamicStyle = ref({ color: 'red' }) | ||
| const focusEvent = ref('focus') | ||
| const onFocus = () => {} | ||
|
|
||
| const items = ref([ | ||
| { id: 1, attr: 'data-foo', value: 'bar', event: 'click', event2: 'dblclick', handler: () => {}, handler2: () => {}, label: 'Item 1' }, | ||
| { id: 2, attr: 'data-baz', value: 'qux', event: 'mouseover', event2: 'mouseout', handler: () => {}, handler2: () => {}, label: 'Item 2' } | ||
| ]) | ||
|
|
||
| const submitEvent = ref('submit') | ||
| const onSubmit = () => {} | ||
|
|
||
| const fields = ref([ | ||
| { name: 'email', prop: 'type', value: 'email', model: 'v-model', data: '', input: 'input', onInput: () => {}, blur: 'blur', onBlur: () => {}, focus: 'focus', onFocus: () => {} }, | ||
| { name: 'password', prop: 'type', value: 'password', model: 'v-model', data: '', input: 'input', onInput: () => {}, blur: 'blur', onBlur: () => {}, focus: 'focus', onFocus: () => {} } | ||
| ]) | ||
|
|
||
| const selectProp = ref('value') | ||
| const selectValue = ref('') | ||
| const changeEvent = ref('change') | ||
| const onChange = () => {} | ||
|
|
||
| const options = ref([ | ||
| { value: 'a', prop: 'value', selected: 'selected', isSelected: false, label: 'Option A' }, | ||
| { value: 'b', prop: 'value', selected: 'selected', isSelected: true, label: 'Option B' } | ||
| ]) | ||
|
|
||
| const clickEvent = ref('click') | ||
| const onTableClick = () => {} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify duplicate declarations and usages of clickEvent in the fixture
rg -n 'const clickEvent' crates/biome_html_parser/benches/fixtures/synthetic/vue-dynamic-args.vue
rg -n '@\[(clickEvent|tableClickEvent)\]' crates/biome_html_parser/benches/fixtures/synthetic/vue-dynamic-args.vueRepository: biomejs/biome
Length of output: 278
Fix duplicate clickEvent const declaration.
const clickEvent is declared twice (lines 130 and 166) in the same script scope, which breaks the fixture.
🛠️ Suggested fix
- <table @[clickEvent].stop="onTableClick">
+ <table @[tableClickEvent].stop="onTableClick">
@@
-const clickEvent = ref('click')
+const tableClickEvent = ref('click')
const onTableClick = () => {}This renames the second declaration and updates its template binding at line 39.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <table @[clickEvent].stop="onTableClick"> | |
| <tr v-for="row in rows" :key="row.id" | |
| :[row.attr]="row.value" | |
| @[row.event].prevent="row.handler" | |
| @[row.event2].capture.once="row.handler2"> | |
| <td v-for="cell in row.cells" :key="cell.id" | |
| :[cell.prop]="cell.value" | |
| @[cell.event].stop.prevent="cell.handler"> | |
| {{ cell.content }} | |
| </td> | |
| </tr> | |
| </table> | |
| <nav @[navEvent].prevent="onNav"> | |
| <a v-for="link in links" :key="link.id" | |
| :[link.href]="link.url" | |
| :[link.target]="link.targetVal" | |
| @[link.click].prevent.stop="link.onClick" | |
| @[link.mouseenter].once="link.onEnter"> | |
| {{ link.text }} | |
| </a> | |
| </nav> | |
| <modal :[modalProp]="modalValue" | |
| @[modalShow]="onShow" | |
| @[modalHide].once="onHide" | |
| @[modalClose].prevent.stop="onClose"> | |
| <header :[headerProp]="headerValue" @[headerEvent]="onHeaderEvent"> | |
| <h2>{{ modalTitle }}</h2> | |
| </header> | |
| <section :[contentProp]="contentValue" @[contentEvent].capture="onContentEvent"> | |
| <p>{{ modalContent }}</p> | |
| </section> | |
| <footer :[footerProp]="footerValue" @[footerEvent].stop="onFooterEvent"> | |
| <button :[actionProp]="actionValue" @[actionEvent].prevent="onAction">Close</button> | |
| </footer> | |
| </modal> | |
| <card v-for="card in cards" :key="card.id" | |
| :[card.titleProp]="card.title" | |
| :[card.imageProp]="card.image" | |
| :[card.classProp]="card.class" | |
| @[card.click].prevent.stop.once="card.onClick" | |
| @[card.hover].capture="card.onHover"> | |
| <template #[card.slotName]> | |
| {{ card.slotContent }} | |
| </template> | |
| </card> | |
| <tabs :[activeTabProp]="activeTab" @[tabChange]="onTabChange"> | |
| <tab v-for="tab in tabs" :key="tab.id" | |
| :[tab.nameProp]="tab.name" | |
| :[tab.disabledProp]="tab.disabled" | |
| @[tab.select].prevent="tab.onSelect" | |
| @[tab.deselect].once="tab.onDeselect"> | |
| {{ tab.content }} | |
| </tab> | |
| </tabs> | |
| <dropdown :[visibleProp]="dropdownVisible" | |
| @[toggleEvent].prevent="toggleDropdown" | |
| @[selectEvent].stop="onSelect" | |
| @[outsideEvent].capture.once="onOutsideClick"> | |
| <item v-for="item in dropdownItems" :key="item.id" | |
| :[item.valueProp]="item.value" | |
| :[item.disabledProp]="item.disabled" | |
| @[item.click].prevent.stop="item.onClick" | |
| @[item.hover]="item.onHover"> | |
| {{ item.label }} | |
| </item> | |
| </dropdown> | |
| <tooltip :[tooltipProp]="tooltipContent" | |
| :[positionProp]="tooltipPosition" | |
| @[showEvent].once="onTooltipShow" | |
| @[hideEvent].prevent="onTooltipHide"> | |
| <span @[triggerEvent]="onTrigger">Hover me</span> | |
| </tooltip> | |
| </div> | |
| </template> | |
| <script setup> | |
| import { ref } from 'vue' | |
| const dynamicProp = ref('value') | |
| const dynamicEvent = ref('input') | |
| const value = ref('test') | |
| const handler = () => {} | |
| const typeProp = ref('type') | |
| const buttonType = ref('button') | |
| const clickEvent = ref('click') | |
| const hoverEvent = ref('mouseenter') | |
| const buttonText = ref('Click me') | |
| const onClick = () => {} | |
| const onHover = () => {} | |
| const classProp = ref('class') | |
| const styleProp = ref('style') | |
| const dynamicClass = ref('dynamic') | |
| const dynamicStyle = ref({ color: 'red' }) | |
| const focusEvent = ref('focus') | |
| const onFocus = () => {} | |
| const items = ref([ | |
| { id: 1, attr: 'data-foo', value: 'bar', event: 'click', event2: 'dblclick', handler: () => {}, handler2: () => {}, label: 'Item 1' }, | |
| { id: 2, attr: 'data-baz', value: 'qux', event: 'mouseover', event2: 'mouseout', handler: () => {}, handler2: () => {}, label: 'Item 2' } | |
| ]) | |
| const submitEvent = ref('submit') | |
| const onSubmit = () => {} | |
| const fields = ref([ | |
| { name: 'email', prop: 'type', value: 'email', model: 'v-model', data: '', input: 'input', onInput: () => {}, blur: 'blur', onBlur: () => {}, focus: 'focus', onFocus: () => {} }, | |
| { name: 'password', prop: 'type', value: 'password', model: 'v-model', data: '', input: 'input', onInput: () => {}, blur: 'blur', onBlur: () => {}, focus: 'focus', onFocus: () => {} } | |
| ]) | |
| const selectProp = ref('value') | |
| const selectValue = ref('') | |
| const changeEvent = ref('change') | |
| const onChange = () => {} | |
| const options = ref([ | |
| { value: 'a', prop: 'value', selected: 'selected', isSelected: false, label: 'Option A' }, | |
| { value: 'b', prop: 'value', selected: 'selected', isSelected: true, label: 'Option B' } | |
| ]) | |
| const clickEvent = ref('click') | |
| const onTableClick = () => {} | |
| <table @[tableClickEvent].stop="onTableClick"> | |
| <tr v-for="row in rows" :key="row.id" | |
| :[row.attr]="row.value" | |
| @[row.event].prevent="row.handler" | |
| @[row.event2].capture.once="row.handler2"> | |
| <td v-for="cell in row.cells" :key="cell.id" | |
| :[cell.prop]="cell.value" | |
| @[cell.event].stop.prevent="cell.handler"> | |
| {{ cell.content }} | |
| </td> | |
| </tr> | |
| </table> | |
| <nav @[navEvent].prevent="onNav"> | |
| <a v-for="link in links" :key="link.id" | |
| :[link.href]="link.url" | |
| :[link.target]="link.targetVal" | |
| @[link.click].prevent.stop="link.onClick" | |
| @[link.mouseenter].once="link.onEnter"> | |
| {{ link.text }} | |
| </a> | |
| </nav> | |
| <modal :[modalProp]="modalValue" | |
| @[modalShow]="onShow" | |
| @[modalHide].once="onHide" | |
| @[modalClose].prevent.stop="onClose"> | |
| <header :[headerProp]="headerValue" @[headerEvent]="onHeaderEvent"> | |
| <h2>{{ modalTitle }}</h2> | |
| </header> | |
| <section :[contentProp]="contentValue" @[contentEvent].capture="onContentEvent"> | |
| <p>{{ modalContent }}</p> | |
| </section> | |
| <footer :[footerProp]="footerValue" @[footerEvent].stop="onFooterEvent"> | |
| <button :[actionProp]="actionValue" @[actionEvent].prevent="onAction">Close</button> | |
| </footer> | |
| </modal> | |
| <card v-for="card in cards" :key="card.id" | |
| :[card.titleProp]="card.title" | |
| :[card.imageProp]="card.image" | |
| :[card.classProp]="card.class" | |
| @[card.click].prevent.stop.once="card.onClick" | |
| @[card.hover].capture="card.onHover"> | |
| <template #[card.slotName]> | |
| {{ card.slotContent }} | |
| </template> | |
| </card> | |
| <tabs :[activeTabProp]="activeTab" @[tabChange]="onTabChange"> | |
| <tab v-for="tab in tabs" :key="tab.id" | |
| :[tab.nameProp]="tab.name" | |
| :[tab.disabledProp]="tab.disabled" | |
| @[tab.select].prevent="tab.onSelect" | |
| @[tab.deselect].once="tab.onDeselect"> | |
| {{ tab.content }} | |
| </tab> | |
| </tabs> | |
| <dropdown :[visibleProp]="dropdownVisible" | |
| @[toggleEvent].prevent="toggleDropdown" | |
| @[selectEvent].stop="onSelect" | |
| @[outsideEvent].capture.once="onOutsideClick"> | |
| <item v-for="item in dropdownItems" :key="item.id" | |
| :[item.valueProp]="item.value" | |
| :[item.disabledProp]="item.disabled" | |
| @[item.click].prevent.stop="item.onClick" | |
| @[item.hover]="item.onHover"> | |
| {{ item.label }} | |
| </item> | |
| </dropdown> | |
| <tooltip :[tooltipProp]="tooltipContent" | |
| :[positionProp]="tooltipPosition" | |
| @[showEvent].once="onTooltipShow" | |
| @[hideEvent].prevent="onTooltipHide"> | |
| <span @[triggerEvent]="onTrigger">Hover me</span> | |
| </tooltip> | |
| </div> | |
| </template> | |
| <script setup> | |
| import { ref } from 'vue' | |
| const dynamicProp = ref('value') | |
| const dynamicEvent = ref('input') | |
| const value = ref('test') | |
| const handler = () => {} | |
| const typeProp = ref('type') | |
| const buttonType = ref('button') | |
| const clickEvent = ref('click') | |
| const hoverEvent = ref('mouseenter') | |
| const buttonText = ref('Click me') | |
| const onClick = () => {} | |
| const onHover = () => {} | |
| const classProp = ref('class') | |
| const styleProp = ref('style') | |
| const dynamicClass = ref('dynamic') | |
| const dynamicStyle = ref({ color: 'red' }) | |
| const focusEvent = ref('focus') | |
| const onFocus = () => {} | |
| const items = ref([ | |
| { id: 1, attr: 'data-foo', value: 'bar', event: 'click', event2: 'dblclick', handler: () => {}, handler2: () => {}, label: 'Item 1' }, | |
| { id: 2, attr: 'data-baz', value: 'qux', event: 'mouseover', event2: 'mouseout', handler: () => {}, handler2: () => {}, label: 'Item 2' } | |
| ]) | |
| const submitEvent = ref('submit') | |
| const onSubmit = () => {} | |
| const fields = ref([ | |
| { name: 'email', prop: 'type', value: 'email', model: 'v-model', data: '', input: 'input', onInput: () => {}, blur: 'blur', onBlur: () => {}, focus: 'focus', onFocus: () => {} }, | |
| { name: 'password', prop: 'type', value: 'password', model: 'v-model', data: '', input: 'input', onInput: () => {}, blur: 'blur', onBlur: () => {}, focus: 'focus', onFocus: () => {} } | |
| ]) | |
| const selectProp = ref('value') | |
| const selectValue = ref('') | |
| const changeEvent = ref('change') | |
| const onChange = () => {} | |
| const options = ref([ | |
| { value: 'a', prop: 'value', selected: 'selected', isSelected: false, label: 'Option A' }, | |
| { value: 'b', prop: 'value', selected: 'selected', isSelected: true, label: 'Option B' } | |
| ]) | |
| const tableClickEvent = ref('click') | |
| const onTableClick = () => {} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/biome_html_parser/benches/fixtures/synthetic/vue-dynamic-args.vue`
around lines 39 - 167, There is a duplicate const declaration for clickEvent;
rename the second script variable (the one used for the table) to a unique name
(e.g., tableClickEvent) and update the template binding that uses it (the table
element with @[clickEvent].stop="onTableClick") to use the new name
@[tableClickEvent]. Ensure you change the declaration (const clickEvent =
ref('click') → const tableClickEvent = ref('click')) and any template references
that pointed to the second clickEvent.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
crates/biome_html_formatter/benches/html_formatter.rs (1)
26-55: Surface fixture I/O failures rather than silently skipping.
load_fixturesdrops unreadable dirs/files without signalling, which can quietly reduce benchmark coverage. Failing fast keeps CI honest.Proposed tweak
- fn visit(dir: &Path, root: &Path, cases: &mut Vec<(String, String, String)>) { - if let Ok(entries) = fs::read_dir(dir) { - for entry in entries.flatten() { + fn visit(dir: &Path, root: &Path, cases: &mut Vec<(String, String, String)>) { + let entries = fs::read_dir(dir) + .unwrap_or_else(|err| panic!("Failed to read fixtures dir {dir:?}: {err}")); + for entry in entries { + let entry = entry + .unwrap_or_else(|err| panic!("Failed to read fixtures entry in {dir:?}: {err}")); let path = entry.path(); if path.is_dir() { visit(&path, root, cases); } else if path.is_file() { if matches!(path.extension().and_then(|e| e.to_str()), Some(ext) if ext.eq_ignore_ascii_case("md")) { continue; } let rel = path.strip_prefix(root).unwrap_or(&path); let group = rel .iter() .next() .and_then(|s| s.to_str()) .unwrap_or("root") .to_string(); let name = path .file_name() .and_then(|s| s.to_str()) .unwrap_or_default() .to_string(); - if let Ok(content) = fs::read_to_string(&path) { - cases.push((group, name, content)); - } + let content = fs::read_to_string(&path) + .unwrap_or_else(|err| panic!("Failed to read fixture {path:?}: {err}")); + cases.push((group, name, content)); } } - } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/biome_html_formatter/benches/html_formatter.rs` around lines 26 - 55, The helper load_fixtures currently swallows I/O errors inside the inner visit function (calls to fs::read_dir and fs::read_to_string) which silently drops fixtures; change load_fixtures (and its inner visit) to surface failures instead of continuing—either by returning Result<Vec<(String,String,String)>, std::io::Error> from load_fixtures and propagating errors from fs::read_dir and fs::read_to_string with ? (adjust visit signature accordingly), or by explicitly panicking with a descriptive message when read_dir/read_to_string returns Err; update all callers to handle the Result if you choose the Result approach.
🤖 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_html_parser/benches/html_parser.rs`:
- Around line 134-157: Pre-validate each fixture before benchmarking by calling
parse_html once (using HtmlParseOptions::from(&file_source) and parse_html) and
collect its diagnostics into a local vec; if diagnostics are non-empty, print a
capped number (e.g., up to 10) using with_file_source_code/from with_file_path
and print_diagnostic_to_string and then skip or fail the benchmark for that
fixture instead of proceeding; then change the closure passed to
group.bench_with_input (the one that currently extends diagnostics) to discard
or black_box only the parse result (do not push into the diagnostics vec) so the
benchmarking loop does not accumulate diagnostics unboundedly.
In `@crates/biome_html_parser/Cargo.toml`:
- Around line 28-31: Update the dev-dependency entry for biome_string_case in
the crate's Cargo.toml to use a path-based reference instead of workspace =
true: replace the existing biome_string_case = { workspace = true } with a
path-based specification (e.g., biome_string_case = { path =
"../biome_string_case" }) so the dev-dependency points at the local crate; leave
the other dev-dependencies (biome_test_utils, camino) unchanged.
---
Nitpick comments:
In `@crates/biome_html_formatter/benches/html_formatter.rs`:
- Around line 26-55: The helper load_fixtures currently swallows I/O errors
inside the inner visit function (calls to fs::read_dir and fs::read_to_string)
which silently drops fixtures; change load_fixtures (and its inner visit) to
surface failures instead of continuing—either by returning
Result<Vec<(String,String,String)>, std::io::Error> from load_fixtures and
propagating errors from fs::read_dir and fs::read_to_string with ? (adjust visit
signature accordingly), or by explicitly panicking with a descriptive message
when read_dir/read_to_string returns Err; update all callers to handle the
Result if you choose the Result approach.
| // Benchmark local fixtures (recursively discovered), including their group names | ||
| for (group_name, name, content) in fixtures { | ||
| let code = content.as_str(); | ||
| let mut diagnostics = vec![]; | ||
| group.throughput(Throughput::Bytes(code.len() as u64)); | ||
| let ext = name | ||
| .rsplit('.') | ||
| .next() | ||
| .unwrap_or_default() | ||
| .to_ascii_lowercase_cow(); | ||
| let file_source = HtmlFileSource::try_from_extension(&ext).unwrap_or_default(); | ||
|
|
||
| let id = format!("{}/{}", group_name, name); | ||
| group.bench_with_input(BenchmarkId::new(&id, "uncached"), &code, |b, _| { | ||
| b.iter(|| { | ||
| let result = black_box(parse_html(code, HtmlParseOptions::from(&file_source))); | ||
| diagnostics.extend(result.into_diagnostics()); | ||
| }) | ||
| }); | ||
|
|
||
| for diagnostic in diagnostics { | ||
| let diagnostic = diagnostic.with_file_source_code(code).with_file_path(&id); | ||
| println!("{}", print_diagnostic_to_string(&diagnostic)); | ||
| } |
There was a problem hiding this comment.
Fail fast on fixture diagnostics to avoid unbounded accumulation.
Lines 147–151 append diagnostics on every iteration, so a faulty fixture will inflate memory and distort timings. Pre-validate once, print a capped set of diagnostics, then benchmark without collecting.
Suggested change
- let mut diagnostics = vec![];
group.throughput(Throughput::Bytes(code.len() as u64));
let ext = name
.rsplit('.')
.next()
.unwrap_or_default()
.to_ascii_lowercase_cow();
let file_source = HtmlFileSource::try_from_extension(&ext).unwrap_or_default();
- let id = format!("{}/{}", group_name, name);
+ let id = format!("{}/{}", group_name, name);
+ let result = parse_html(code, HtmlParseOptions::from(&file_source));
+ if !result.diagnostics().is_empty() {
+ let truncated = result
+ .into_diagnostics()
+ .into_iter()
+ .take(20)
+ .collect::<Vec<_>>();
+ for diagnostic in truncated {
+ let diagnostic = diagnostic.with_file_source_code(code).with_file_path(&id);
+ println!("{}", print_diagnostic_to_string(&diagnostic));
+ }
+ panic!(
+ "Parsing errors detected in benchmark. Only some of the diagnostics are printed above."
+ );
+ }
group.bench_with_input(BenchmarkId::new(&id, "uncached"), &code, |b, _| {
b.iter(|| {
- let result = black_box(parse_html(code, HtmlParseOptions::from(&file_source)));
- diagnostics.extend(result.into_diagnostics());
+ black_box(parse_html(code, HtmlParseOptions::from(&file_source)));
})
});
-
- for diagnostic in diagnostics {
- let diagnostic = diagnostic.with_file_source_code(code).with_file_path(&id);
- println!("{}", print_diagnostic_to_string(&diagnostic));
- }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/biome_html_parser/benches/html_parser.rs` around lines 134 - 157,
Pre-validate each fixture before benchmarking by calling parse_html once (using
HtmlParseOptions::from(&file_source) and parse_html) and collect its diagnostics
into a local vec; if diagnostics are non-empty, print a capped number (e.g., up
to 10) using with_file_source_code/from with_file_path and
print_diagnostic_to_string and then skip or fail the benchmark for that fixture
instead of proceeding; then change the closure passed to group.bench_with_input
(the one that currently extends diagnostics) to discard or black_box only the
parse result (do not push into the diagnostics vec) so the benchmarking loop
does not accumulate diagnostics unboundedly.
| [dev-dependencies] | ||
| biome_string_case = { workspace = true } | ||
| biome_test_utils = { workspace = true } | ||
| camino = { workspace = true } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find biome_string_case location and check workspace configuration
find . -name "Cargo.toml" -path "*/biome_string_case/*" | head -5
# Check if biome_string_case is defined in workspace
rg "biome_string_case" -g 'Cargo.toml' -B 2 -A 2 | head -40
# Check the workspace root Cargo.toml for dependency definitions
head -100 Cargo.toml | grep -A 50 "dependencies"
# See how other crates handle dev-dependencies
rg "\[dev-dependencies\]" -A 10 -g 'Cargo.toml' crates/ | head -60Repository: biomejs/biome
Length of output: 10350
Switch biome_string_case to a path dev-dependency.
Dev-dependencies should use path-based references to avoid requiring published versions. Change line 29 from workspace to path.
Suggested change
-biome_string_case = { workspace = true }
+biome_string_case = { path = "../biome_string_case" }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| [dev-dependencies] | |
| biome_string_case = { workspace = true } | |
| biome_test_utils = { workspace = true } | |
| camino = { workspace = true } | |
| [dev-dependencies] | |
| biome_string_case = { path = "../biome_string_case" } | |
| biome_test_utils = { workspace = true } | |
| camino = { workspace = true } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/biome_html_parser/Cargo.toml` around lines 28 - 31, Update the
dev-dependency entry for biome_string_case in the crate's Cargo.toml to use a
path-based reference instead of workspace = true: replace the existing
biome_string_case = { workspace = true } with a path-based specification (e.g.,
biome_string_case = { path = "../biome_string_case" }) so the dev-dependency
points at the local crate; leave the other dev-dependencies (biome_test_utils,
camino) unchanged.

Summary
I wanted to expand our HTML benchmarks to get better signals on parsing/formatting perf.
#6623 is preventing me from landing this.most of the synthetic benchmarks are AI generated.
Test Plan
CI should be green.
Docs