diff --git a/Cargo.lock b/Cargo.lock index 39d802bcaeee..b815c476f377 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -226,6 +226,25 @@ dependencies = [ "rustc-hash", ] +[[package]] +name = "biome_css_analyze" +version = "0.4.0" +dependencies = [ + "biome_analyze", + "biome_console", + "biome_css_factory", + "biome_css_parser", + "biome_css_syntax", + "biome_diagnostics", + "biome_rowan", + "biome_service", + "biome_test_utils", + "insta", + "lazy_static", + "rustc-hash", + "tests_macros", +] + [[package]] name = "biome_css_factory" version = "0.3.1" diff --git a/crates/biome_analyze/src/diagnostics.rs b/crates/biome_analyze/src/diagnostics.rs index 019e0a4f0bf3..efec6d670faf 100644 --- a/crates/biome_analyze/src/diagnostics.rs +++ b/crates/biome_analyze/src/diagnostics.rs @@ -138,9 +138,9 @@ impl AnalyzerDiagnostic { } } -#[derive(Debug, Diagnostic)] +#[derive(Debug, Diagnostic, Clone)] #[diagnostic(severity = Warning)] -pub(crate) struct SuppressionDiagnostic { +pub struct SuppressionDiagnostic { #[category] category: &'static Category, #[location(span)] diff --git a/crates/biome_analyze/src/lib.rs b/crates/biome_analyze/src/lib.rs index 549f3b59dc48..7b88964e7f9d 100644 --- a/crates/biome_analyze/src/lib.rs +++ b/crates/biome_analyze/src/lib.rs @@ -26,7 +26,7 @@ pub use crate::categories::{ ActionCategory, RefactorKind, RuleCategories, RuleCategory, SourceActionKind, }; pub use crate::diagnostics::AnalyzerDiagnostic; -use crate::diagnostics::SuppressionDiagnostic; +pub use crate::diagnostics::SuppressionDiagnostic; pub use crate::matcher::{InspectMatcher, MatchQueryParams, QueryMatcher, RuleKey, SignalEntry}; pub use crate::options::{AnalyzerConfiguration, AnalyzerOptions, AnalyzerRules}; pub use crate::query::{AddVisitor, QueryKey, QueryMatch, Queryable}; diff --git a/crates/biome_css_analyze/Cargo.toml b/crates/biome_css_analyze/Cargo.toml new file mode 100644 index 000000000000..080dda5b990a --- /dev/null +++ b/crates/biome_css_analyze/Cargo.toml @@ -0,0 +1,33 @@ +[package] +authors.workspace = true +categories.workspace = true +description = "Biome's CSS linter" +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +name = "biome_css_analyze" +repository.workspace = true +version = "0.4.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +biome_analyze = { workspace = true } +biome_console = { workspace = true } +biome_css_syntax = { workspace = true } +biome_diagnostics = { workspace = true } +biome_rowan = { workspace = true } +lazy_static = { workspace = true } +rustc-hash = { workspace = true } + +[dev-dependencies] +biome_css_factory = { path = "../biome_css_factory" } +biome_css_parser = { path = "../biome_css_parser" } +biome_service = { path = "../biome_service" } +biome_test_utils = { path = "../biome_test_utils" } +insta = { workspace = true, features = ["glob"] } +tests_macros = { path = "../tests_macros" } + +[lints] +workspace = true diff --git a/crates/biome_css_analyze/src/analyzers.rs b/crates/biome_css_analyze/src/analyzers.rs new file mode 100644 index 000000000000..2112770ad47c --- /dev/null +++ b/crates/biome_css_analyze/src/analyzers.rs @@ -0,0 +1,4 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +pub(crate) mod nursery; +::biome_analyze::declare_category! { pub (crate) Analyzers { kind : Lint , groups : [self :: nursery :: Nursery ,] } } diff --git a/crates/biome_css_analyze/src/analyzers/nursery.rs b/crates/biome_css_analyze/src/analyzers/nursery.rs new file mode 100644 index 000000000000..6b3f0eb15794 --- /dev/null +++ b/crates/biome_css_analyze/src/analyzers/nursery.rs @@ -0,0 +1,14 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +use biome_analyze::declare_group; + +pub(crate) mod noop; + +declare_group! { + pub (crate) Nursery { + name : "nursery" , + rules : [ + self :: noop :: Noop , + ] + } +} diff --git a/crates/biome_css_analyze/src/analyzers/nursery/noop.rs b/crates/biome_css_analyze/src/analyzers/nursery/noop.rs new file mode 100644 index 000000000000..626d7a44397a --- /dev/null +++ b/crates/biome_css_analyze/src/analyzers/nursery/noop.rs @@ -0,0 +1,21 @@ +use biome_analyze::{context::RuleContext, declare_rule, Ast, Rule}; +use biome_css_syntax::CssColor; + +declare_rule! { + /// Noop rule + pub(crate) Noop { + version: "next", + name: "noop", + } +} + +impl Rule for Noop { + type Query = Ast; + type State = (); + type Signals = Option; + type Options = (); + + fn run(_: &RuleContext) -> Option { + None + } +} diff --git a/crates/biome_css_analyze/src/lib.rs b/crates/biome_css_analyze/src/lib.rs new file mode 100644 index 000000000000..353aa6af6860 --- /dev/null +++ b/crates/biome_css_analyze/src/lib.rs @@ -0,0 +1,161 @@ +mod analyzers; +mod registry; + +pub use crate::registry::visit_registry; +use biome_analyze::{ + AnalysisFilter, AnalyzerOptions, AnalyzerSignal, ControlFlow, LanguageRoot, MatchQueryParams, + MetadataRegistry, RuleRegistry, SuppressionDiagnostic, SuppressionKind, +}; +use biome_css_syntax::CssLanguage; +use biome_diagnostics::Error; + +/// Return the static [MetadataRegistry] for the JSON analyzer rules +pub fn metadata() -> &'static MetadataRegistry { + lazy_static::lazy_static! { + static ref METADATA: MetadataRegistry = { + let mut metadata = MetadataRegistry::default(); + visit_registry(&mut metadata); + metadata + }; + } + + &METADATA +} + +/// Run the analyzer on the provided `root`: this process will use the given `filter` +/// to selectively restrict analysis to specific rules / a specific source range, +/// then call `emit_signal` when an analysis rule emits a diagnostic or action +pub fn analyze<'a, F, B>( + root: &LanguageRoot, + filter: AnalysisFilter, + options: &'a AnalyzerOptions, + emit_signal: F, +) -> (Option, Vec) +where + F: FnMut(&dyn AnalyzerSignal) -> ControlFlow + 'a, + B: 'a, +{ + analyze_with_inspect_matcher(root, filter, |_| {}, options, emit_signal) +} + +/// Run the analyzer on the provided `root`: this process will use the given `filter` +/// to selectively restrict analysis to specific rules / a specific source range, +/// then call `emit_signal` when an analysis rule emits a diagnostic or action. +/// Additionally, this function takes a `inspect_matcher` function that can be +/// used to inspect the "query matches" emitted by the analyzer before they are +/// processed by the lint rules registry +pub fn analyze_with_inspect_matcher<'a, V, F, B>( + root: &LanguageRoot, + filter: AnalysisFilter, + inspect_matcher: V, + options: &'a AnalyzerOptions, + mut emit_signal: F, +) -> (Option, Vec) +where + V: FnMut(&MatchQueryParams) + 'a, + F: FnMut(&dyn AnalyzerSignal) -> ControlFlow + 'a, + B: 'a, +{ + fn parse_linter_suppression_comment( + _text: &str, + ) -> Vec> { + vec![] + } + let mut registry = RuleRegistry::builder(&filter, root); + visit_registry(&mut registry); + + let (registry, services, diagnostics, visitors) = registry.build(); + + // Bail if we can't parse a rule option + if !diagnostics.is_empty() { + return (None, diagnostics); + } + + let mut analyzer = biome_analyze::Analyzer::new( + metadata(), + biome_analyze::InspectMatcher::new(registry, inspect_matcher), + parse_linter_suppression_comment, + |_| {}, + &mut emit_signal, + ); + + for ((phase, _), visitor) in visitors { + analyzer.add_visitor(phase, visitor); + } + + ( + analyzer.run(biome_analyze::AnalyzerContext { + root: root.clone(), + range: filter.range, + services, + options, + }), + diagnostics, + ) +} + +#[cfg(test)] +mod tests { + use biome_analyze::{AnalyzerOptions, Never, RuleFilter}; + use biome_console::fmt::{Formatter, Termcolor}; + use biome_console::{markup, Markup}; + use biome_css_parser::{parse_css, CssParserOptions}; + use biome_css_syntax::TextRange; + use biome_diagnostics::termcolor::NoColor; + use biome_diagnostics::{Diagnostic, DiagnosticExt, PrintDiagnostic, Severity}; + use std::slice; + + use crate::{analyze, AnalysisFilter, ControlFlow}; + + #[ignore] + #[test] + fn quick_test() { + fn markup_to_string(markup: Markup) -> String { + let mut buffer = Vec::new(); + let mut write = Termcolor(NoColor::new(&mut buffer)); + let mut fmt = Formatter::new(&mut write); + fmt.write_markup(markup).unwrap(); + + String::from_utf8(buffer).unwrap() + } + + const SOURCE: &str = r#".something {} +"#; + + let parsed = parse_css(SOURCE, CssParserOptions::default()); + + let mut error_ranges: Vec = Vec::new(); + let rule_filter = RuleFilter::Rule("nursery", "noDuplicateKeys"); + let options = AnalyzerOptions::default(); + analyze( + &parsed.tree(), + AnalysisFilter { + enabled_rules: Some(slice::from_ref(&rule_filter)), + ..AnalysisFilter::default() + }, + &options, + |signal| { + if let Some(diag) = signal.diagnostic() { + error_ranges.push(diag.location().span.unwrap()); + let error = diag + .with_severity(Severity::Warning) + .with_file_path("ahahah") + .with_file_source_code(SOURCE); + let text = markup_to_string(markup! { + {PrintDiagnostic::verbose(&error)} + }); + eprintln!("{text}"); + } + + for action in signal.actions() { + let new_code = action.mutation.commit(); + eprintln!("{new_code}"); + } + + ControlFlow::::Continue(()) + }, + ); + + assert_eq!(error_ranges.as_slice(), &[]); + } +} diff --git a/crates/biome_css_analyze/src/registry.rs b/crates/biome_css_analyze/src/registry.rs new file mode 100644 index 000000000000..c2de238e01e9 --- /dev/null +++ b/crates/biome_css_analyze/src/registry.rs @@ -0,0 +1,7 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +use biome_analyze::RegistryVisitor; +use biome_css_syntax::CssLanguage; +pub fn visit_registry>(registry: &mut V) { + registry.record_category::(); +} diff --git a/crates/biome_css_analyze/tests/spec_tests.rs b/crates/biome_css_analyze/tests/spec_tests.rs new file mode 100644 index 000000000000..50378d7553f8 --- /dev/null +++ b/crates/biome_css_analyze/tests/spec_tests.rs @@ -0,0 +1,250 @@ +use biome_analyze::{AnalysisFilter, AnalyzerAction, ControlFlow, Never, RuleFilter}; +use biome_css_parser::{parse_css, CssParserOptions}; +use biome_css_syntax::{CssFileSource, CssLanguage}; +use biome_diagnostics::advice::CodeSuggestionAdvice; +use biome_diagnostics::{DiagnosticExt, Severity}; +use biome_rowan::AstNode; +use biome_test_utils::{ + assert_errors_are_absent, code_fix_to_string, create_analyzer_options, diagnostic_to_string, + has_bogus_nodes_or_empty_slots, parse_test_path, register_leak_checker, scripts_from_json, + write_analyzer_snapshot, CheckActionType, +}; +use std::{ffi::OsStr, fs::read_to_string, path::Path, slice}; + +tests_macros::gen_tests! {"tests/specs/**/*.{css,json,jsonc}", crate::run_test, "module"} +tests_macros::gen_tests! {"tests/suppression/**/*.{css,json,jsonc}", crate::run_suppression_test, "module"} + +fn run_test(input: &'static str, _: &str, _: &str, _: &str) { + register_leak_checker(); + + let input_file = Path::new(input); + let file_name = input_file.file_name().and_then(OsStr::to_str).unwrap(); + + let (group, rule) = parse_test_path(input_file); + if rule == "specs" || rule == "suppression" { + panic!("the test file must be placed in the {rule}/// directory"); + } + if group == "specs" || group == "suppression" { + panic!("the test file must be placed in the {group}/{rule}// directory"); + } + if biome_css_analyze::metadata() + .find_rule(group, rule) + .is_none() + { + panic!("could not find rule {group}/{rule}"); + } + + let rule_filter = RuleFilter::Rule(group, rule); + let filter = AnalysisFilter { + enabled_rules: Some(slice::from_ref(&rule_filter)), + ..AnalysisFilter::default() + }; + + let mut snapshot = String::new(); + let extension = input_file.extension().unwrap_or_default(); + + let input_code = read_to_string(input_file) + .unwrap_or_else(|err| panic!("failed to read {:?}: {:?}", input_file, err)); + let quantity_diagnostics = if let Some(scripts) = scripts_from_json(extension, &input_code) { + for script in scripts { + analyze_and_snap( + &mut snapshot, + &script, + CssFileSource::css(), + filter, + file_name, + input_file, + CheckActionType::Lint, + CssParserOptions::default(), + ); + } + + 0 + } else { + let Ok(source_type) = input_file.try_into() else { + return; + }; + analyze_and_snap( + &mut snapshot, + &input_code, + source_type, + filter, + file_name, + input_file, + CheckActionType::Lint, + CssParserOptions::default(), + ) + }; + + insta::with_settings!({ + prepend_module_to_snapshot => false, + snapshot_path => input_file.parent().unwrap(), + }, { + insta::assert_snapshot!(file_name, snapshot, file_name); + }); + + if input_code.contains("/* should not generate diagnostics */") && quantity_diagnostics > 0 { + panic!("This test should not generate diagnostics"); + } +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn analyze_and_snap( + snapshot: &mut String, + input_code: &str, + source_type: CssFileSource, + filter: AnalysisFilter, + file_name: &str, + input_file: &Path, + check_action_type: CheckActionType, + parser_options: CssParserOptions, +) -> usize { + let parsed = parse_css(input_code, parser_options.clone()); + let root = parsed.tree(); + + let mut diagnostics = Vec::new(); + let mut code_fixes = Vec::new(); + let options = create_analyzer_options(input_file, &mut diagnostics); + + let (_, errors) = biome_css_analyze::analyze(&root, filter, &options, |event| { + if let Some(mut diag) = event.diagnostic() { + for action in event.actions() { + if check_action_type.is_suppression() { + if action.is_suppression() { + check_code_action( + input_file, + input_code, + source_type, + &action, + parser_options.clone(), + ); + diag = diag.add_code_suggestion(CodeSuggestionAdvice::from(action)); + } + } else if !action.is_suppression() { + check_code_action( + input_file, + input_code, + source_type, + &action, + parser_options.clone(), + ); + diag = diag.add_code_suggestion(CodeSuggestionAdvice::from(action)); + } + } + + let error = diag.with_severity(Severity::Warning); + diagnostics.push(diagnostic_to_string(file_name, input_code, error)); + return ControlFlow::Continue(()); + } + + for action in event.actions() { + if check_action_type.is_suppression() { + if action.category.matches("quickfix.suppressRule") { + check_code_action( + input_file, + input_code, + source_type, + &action, + parser_options.clone(), + ); + code_fixes.push(code_fix_to_string(input_code, action)); + } + } else if !action.category.matches("quickfix.suppressRule") { + check_code_action( + input_file, + input_code, + source_type, + &action, + parser_options.clone(), + ); + code_fixes.push(code_fix_to_string(input_code, action)); + } + } + + ControlFlow::::Continue(()) + }); + + for error in errors { + diagnostics.push(diagnostic_to_string(file_name, input_code, error)); + } + + write_analyzer_snapshot( + snapshot, + input_code, + diagnostics.as_slice(), + code_fixes.as_slice(), + "css", + ); + + diagnostics.len() +} + +fn check_code_action( + path: &Path, + source: &str, + _source_type: CssFileSource, + action: &AnalyzerAction, + options: CssParserOptions, +) { + let (_, text_edit) = action.mutation.as_text_edits().unwrap_or_default(); + + let output = text_edit.new_string(source); + + let new_tree = action.mutation.clone().commit(); + + // Checks that applying the text edits returned by the BatchMutation + // returns the same code as printing the modified syntax tree + assert_eq!(new_tree.to_string(), output); + + if has_bogus_nodes_or_empty_slots(&new_tree) { + panic!( + "modified tree has bogus nodes or empty slots:\n{new_tree:#?} \n\n {}", + new_tree + ) + } + + // Checks the returned tree contains no missing children node + if format!("{new_tree:?}").contains("missing (required)") { + panic!("modified tree has missing children:\n{new_tree:#?}") + } + + // Re-parse the modified code and panic if the resulting tree has syntax errors + let re_parse = parse_css(&output, options); + assert_errors_are_absent(re_parse.tree().syntax(), re_parse.diagnostics(), path); +} + +pub(crate) fn _run_suppression_test(input: &'static str, _: &str, _: &str, _: &str) { + register_leak_checker(); + + let input_file = Path::new(input); + let file_name = input_file.file_name().and_then(OsStr::to_str).unwrap(); + let input_code = read_to_string(input_file) + .unwrap_or_else(|err| panic!("failed to read {:?}: {:?}", input_file, err)); + + let (group, rule) = parse_test_path(input_file); + + let rule_filter = RuleFilter::Rule(group, rule); + let filter = AnalysisFilter { + enabled_rules: Some(slice::from_ref(&rule_filter)), + ..AnalysisFilter::default() + }; + + let mut snapshot = String::new(); + analyze_and_snap( + &mut snapshot, + &input_code, + CssFileSource::css(), + filter, + file_name, + input_file, + CheckActionType::Suppression, + CssParserOptions::default(), + ); + + insta::with_settings!({ + prepend_module_to_snapshot => false, + snapshot_path => input_file.parent().unwrap(), + }, { + insta::assert_snapshot!(file_name, snapshot, file_name); + }); +} diff --git a/crates/biome_css_analyze/tests/specs/nursery/noop/invalid.css b/crates/biome_css_analyze/tests/specs/nursery/noop/invalid.css new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/crates/biome_css_analyze/tests/specs/nursery/noop/invalid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noop/invalid.css.snap new file mode 100644 index 000000000000..485142971611 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noop/invalid.css.snap @@ -0,0 +1,10 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: invalid.css +--- +# Input +```css + +``` + + diff --git a/crates/biome_css_analyze/tests/specs/nursery/noop/valid.css b/crates/biome_css_analyze/tests/specs/nursery/noop/valid.css new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/crates/biome_css_analyze/tests/specs/nursery/noop/valid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noop/valid.css.snap new file mode 100644 index 000000000000..e20fb30f2281 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noop/valid.css.snap @@ -0,0 +1,10 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: valid.css +--- +# Input +```css + +``` + + diff --git a/crates/biome_css_parser/src/parser.rs b/crates/biome_css_parser/src/parser.rs index 4daf37df363a..9bec45588c23 100644 --- a/crates/biome_css_parser/src/parser.rs +++ b/crates/biome_css_parser/src/parser.rs @@ -14,7 +14,7 @@ pub(crate) struct CssParser<'source> { state: CssParserState, } -#[derive(Default, Debug, Clone, Copy)] +#[derive(Default, Debug, Clone)] pub struct CssParserOptions { pub allow_wrong_line_comments: bool, } diff --git a/crates/biome_css_syntax/src/file_source.rs b/crates/biome_css_syntax/src/file_source.rs index 84684b7960df..d137438fb765 100644 --- a/crates/biome_css_syntax/src/file_source.rs +++ b/crates/biome_css_syntax/src/file_source.rs @@ -2,7 +2,7 @@ use crate::CssLanguage; use biome_rowan::{FileSource, FileSourceError}; use std::path::Path; -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, Copy)] pub struct CssFileSource { // Unused until we potentially support postcss/less/sass #[allow(unused)] @@ -13,7 +13,7 @@ pub struct CssFileSource { /// /// Currently, Biome only supports plain CSS, and aims to be compatible with /// the latest Recommendation level standards. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, Copy)] enum CssVariant { #[default] Standard, diff --git a/crates/biome_js_analyze/tests/spec_tests.rs b/crates/biome_js_analyze/tests/spec_tests.rs index 49bff7a2e5d8..dbdba05c4d00 100644 --- a/crates/biome_js_analyze/tests/spec_tests.rs +++ b/crates/biome_js_analyze/tests/spec_tests.rs @@ -173,6 +173,7 @@ pub(crate) fn analyze_and_snap( input_code, diagnostics.as_slice(), code_fixes.as_slice(), + source_type.file_extension(), ); diagnostics.len() diff --git a/crates/biome_js_analyze/tests/specs/a11y/noAccessKey/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/a11y/noAccessKey/invalid.jsx.snap index 3fca7f63dec5..fd529a50c2e9 100644 --- a/crates/biome_js_analyze/tests/specs/a11y/noAccessKey/invalid.jsx.snap +++ b/crates/biome_js_analyze/tests/specs/a11y/noAccessKey/invalid.jsx.snap @@ -3,7 +3,7 @@ source: crates/biome_js_analyze/tests/spec_tests.rs expression: invalid.jsx --- # Input -```js +```jsx ; ;
; diff --git a/crates/biome_js_analyze/tests/specs/a11y/noAccessKey/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/a11y/noAccessKey/valid.jsx.snap index 5ccbe7cd798a..3a97267bc58d 100644 --- a/crates/biome_js_analyze/tests/specs/a11y/noAccessKey/valid.jsx.snap +++ b/crates/biome_js_analyze/tests/specs/a11y/noAccessKey/valid.jsx.snap @@ -3,7 +3,7 @@ source: crates/biome_js_analyze/tests/spec_tests.rs expression: valid.jsx --- # Input -```js +```jsx ; ; ; diff --git a/crates/biome_js_analyze/tests/specs/a11y/noAriaUnsupportedElements/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/a11y/noAriaUnsupportedElements/invalid.jsx.snap index 73e6da7a9cb3..2499eea49746 100644 --- a/crates/biome_js_analyze/tests/specs/a11y/noAriaUnsupportedElements/invalid.jsx.snap +++ b/crates/biome_js_analyze/tests/specs/a11y/noAriaUnsupportedElements/invalid.jsx.snap @@ -3,7 +3,7 @@ source: crates/biome_js_analyze/tests/spec_tests.rs expression: invalid.jsx --- # Input -```js +```jsx <> diff --git a/crates/biome_js_analyze/tests/specs/a11y/noAriaUnsupportedElements/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/a11y/noAriaUnsupportedElements/valid.jsx.snap index 9bc0622ca7ec..f94bc9df75d9 100644 --- a/crates/biome_js_analyze/tests/specs/a11y/noAriaUnsupportedElements/valid.jsx.snap +++ b/crates/biome_js_analyze/tests/specs/a11y/noAriaUnsupportedElements/valid.jsx.snap @@ -3,7 +3,7 @@ source: crates/biome_js_analyze/tests/spec_tests.rs expression: valid.jsx --- # Input -```js +```jsx /* should not generate diagnostics */ <> diff --git a/crates/biome_js_analyze/tests/specs/a11y/noAutofocus/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/a11y/noAutofocus/invalid.jsx.snap index f14e3d651fa7..ae200810008c 100644 --- a/crates/biome_js_analyze/tests/specs/a11y/noAutofocus/invalid.jsx.snap +++ b/crates/biome_js_analyze/tests/specs/a11y/noAutofocus/invalid.jsx.snap @@ -3,7 +3,7 @@ source: crates/biome_js_analyze/tests/spec_tests.rs expression: invalid.jsx --- # Input -```js +```jsx <>