diff --git a/CHANGELOG.md b/CHANGELOG.md index 5854b0aa532b..64b37befe623 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,28 @@ New entries must be placed in a section entitled `Unreleased`. Read our [guidelines for writing a good changelog entry](https://github.com/biomejs/biome/blob/main/CONTRIBUTING.md#changelog). +## Unreleased + +### Analyzer + +### CLI + +### Configuration + +### Editors + +### Formatter + +### JavaScript APIs + +### Linter + +#### Bug fixes + +- The [`noUnmatchableAnbSelector`](https://biomejs.dev/linter/rules/no-unmatchable-anb-selector/) rule is now able to catch unmatchable `an+b` selectors like `0n+0` or `-0n+0`. Contributed by @Sec-ant. + +### Parser + ## v1.8.1 (2024-06-10) ### Analyzer diff --git a/crates/biome_configuration/src/linter/rules.rs b/crates/biome_configuration/src/linter/rules.rs index f24ad9f9b521..735c20069e8d 100644 --- a/crates/biome_configuration/src/linter/rules.rs +++ b/crates/biome_configuration/src/linter/rules.rs @@ -2905,7 +2905,7 @@ pub struct Nursery { #[serde(skip_serializing_if = "Option::is_none")] pub use_consistent_builtin_instantiation: Option>, - #[doc = "Disallowing invalid named grid areas in CSS Grid Layouts."] + #[doc = "Disallows invalid named grid areas in CSS Grid Layouts."] #[serde(skip_serializing_if = "Option::is_none")] pub use_consistent_grid_areas: Option>, #[doc = "Use Date.now() to get the number of milliseconds since the Unix Epoch."] diff --git a/crates/biome_css_analyze/src/lint/nursery/no_unmatchable_anb_selector.rs b/crates/biome_css_analyze/src/lint/nursery/no_unmatchable_anb_selector.rs index 76cc88192b0f..6d042df9edc9 100644 --- a/crates/biome_css_analyze/src/lint/nursery/no_unmatchable_anb_selector.rs +++ b/crates/biome_css_analyze/src/lint/nursery/no_unmatchable_anb_selector.rs @@ -98,7 +98,8 @@ fn is_unmatchable(nth: &AnyCssPseudoClassNth) -> bool { AnyCssPseudoClassNth::CssPseudoClassNthIdentifier(_) => false, AnyCssPseudoClassNth::CssPseudoClassNth(nth) => { let coefficient = nth.value(); - let constant = nth.offset(); + let constant = nth.offset().and_then(|offset| offset.value().ok()); + match (coefficient, constant) { (Some(a), Some(b)) => a.text() == "0" && b.text() == "0", (Some(a), None) => a.text() == "0", diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnmatchableAnbSelector/invalid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noUnmatchableAnbSelector/invalid.css.snap index 6e390f281647..3cceb27bb2cf 100644 --- a/crates/biome_css_analyze/tests/specs/nursery/noUnmatchableAnbSelector/invalid.css.snap +++ b/crates/biome_css_analyze/tests/specs/nursery/noUnmatchableAnbSelector/invalid.css.snap @@ -93,6 +93,63 @@ invalid.css:4:13 lint/nursery/noUnmatchableAnbSelector ━━━━━━━━ i For more details, see the official spec for An+B selectors. +``` + +``` +invalid.css:5:13 lint/nursery/noUnmatchableAnbSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This selector will never match any elements. + + 3 │ a:nth-child(+0n) {} + 4 │ a:nth-child(-0n) {} + > 5 │ a:nth-child(0n+0) {} + │ ^^^^ + 6 │ a:nth-child(0n-0) {} + 7 │ a:nth-child(-0n-0) {} + + i Avoid using An+B selectors that always evaluate to 0. + + i For more details, see the official spec for An+B selectors. + + +``` + +``` +invalid.css:6:13 lint/nursery/noUnmatchableAnbSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This selector will never match any elements. + + 4 │ a:nth-child(-0n) {} + 5 │ a:nth-child(0n+0) {} + > 6 │ a:nth-child(0n-0) {} + │ ^^^^ + 7 │ a:nth-child(-0n-0) {} + 8 │ a:nth-child(0 of a) {} + + i Avoid using An+B selectors that always evaluate to 0. + + i For more details, see the official spec for An+B selectors. + + +``` + +``` +invalid.css:7:13 lint/nursery/noUnmatchableAnbSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This selector will never match any elements. + + 5 │ a:nth-child(0n+0) {} + 6 │ a:nth-child(0n-0) {} + > 7 │ a:nth-child(-0n-0) {} + │ ^^^^^ + 8 │ a:nth-child(0 of a) {} + 9 │ a:nth-child(0), a:nth-child(1) {} + + i Avoid using An+B selectors that always evaluate to 0. + + i For more details, see the official spec for An+B selectors. + + ``` ``` diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 03c433252930..9aea7a38aa33 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1099,7 +1099,7 @@ export interface Nursery { */ useConsistentBuiltinInstantiation?: RuleFixConfiguration_for_Null; /** - * Disallowing invalid named grid areas in CSS Grid Layouts. + * Disallows invalid named grid areas in CSS Grid Layouts. */ useConsistentGridAreas?: RuleConfiguration_for_Null; /** diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index acb99a340da5..36c2d8547797 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1879,7 +1879,7 @@ ] }, "useConsistentGridAreas": { - "description": "Disallowing invalid named grid areas in CSS Grid Layouts.", + "description": "Disallows invalid named grid areas in CSS Grid Layouts.", "anyOf": [ { "$ref": "#/definitions/RuleConfiguration" }, { "type": "null" } diff --git a/xtask/rules_check/src/lib.rs b/xtask/rules_check/src/lib.rs index 124e53284863..8f55318caf82 100644 --- a/xtask/rules_check/src/lib.rs +++ b/xtask/rules_check/src/lib.rs @@ -4,8 +4,8 @@ use anyhow::{bail, ensure}; use biome_analyze::options::JsxRuntime; use biome_analyze::{ - AnalysisFilter, AnalyzerOptions, FixKind, GroupCategory, Queryable, RegistryVisitor, Rule, - RuleCategory, RuleFilter, RuleGroup, RuleMetadata, + AnalysisFilter, AnalyzerOptions, GroupCategory, Queryable, RegistryVisitor, Rule, RuleCategory, + RuleFilter, RuleGroup, RuleMetadata, }; use biome_console::{markup, Console}; use biome_css_parser::CssParserOptions; @@ -99,12 +99,7 @@ pub fn check_rules() -> anyhow::Result<()> { for (group, rules) in groups { for (_, meta) in rules { - parse_documentation( - group, - meta.name, - meta.docs, - !matches!(meta.fix_kind, FixKind::None), - )?; + parse_documentation(group, meta.name, meta.docs)?; } } @@ -201,17 +196,12 @@ fn assert_lint( rule: &'static str, test: &CodeBlockTest, code: &str, - has_fix_kind: bool, ) -> anyhow::Result<()> { - let file = format!("{group}/{rule}.js"); + let file = "rule-doc-code-block"; let mut diagnostic_count = 0; - let mut all_diagnostics = vec![]; - let mut write_diagnostic = |code: &str, diag: biome_diagnostics::Error| { - let category = diag.category().map_or("", |code| code.name()); - all_diagnostics.push(diag); // Fail the test if the analysis returns more diagnostics than expected if test.expect_diagnostic { @@ -226,13 +216,8 @@ fn assert_lint( }, ); } + bail!("Analysis of '{group}/{rule}' on the following code block returned multiple diagnostics.\n\n{code}"); } - - ensure!( - diagnostic_count == 0, - "analysis returned multiple diagnostics, code snippet: \n\n{}", - code - ); } else { // Print all diagnostics to help the user let mut console = biome_console::EnvConsole::default(); @@ -244,20 +229,16 @@ fn assert_lint( }, ); } - - bail!(format!( - "analysis returned an unexpected diagnostic, code `snippet:\n\n{:?}\n\n{}", - category, code - )); + bail!("Analysis of '{group}/{rule}' on the following code block returned an unexpected diagnostic.\n\n{code}"); } - diagnostic_count += 1; Ok(()) }; + if test.ignore { return Ok(()); } - let mut rule_has_code_action = false; + let mut settings = WorkspaceSettings::default(); let key = settings.insert_project(PathBuf::new()); settings.register_current_project(key); @@ -284,9 +265,7 @@ fn assert_lint( if parse.has_errors() { for diag in parse.into_diagnostics() { - let error = diag - .with_file_path(file.clone()) - .with_file_source_code(code); + let error = diag.with_file_path(file).with_file_source_code(code); write_diagnostic(code, error)?; } } else { @@ -300,61 +279,34 @@ fn assert_lint( let mut options = AnalyzerOptions::default(); options.configuration.jsx_runtime = Some(JsxRuntime::default()); - let (_, diagnostics) = biome_js_analyze::analyze( - &root, - filter, - &options, - source_type, - None, - |signal| { - if let Some(mut diag) = signal.diagnostic() { - let category = diag.category().expect("linter diagnostic has no code"); - let severity = settings.get_current_settings().expect("project").get_severity_from_rule_code(category).expect( + biome_js_analyze::analyze(&root, filter, &options, source_type, None, |signal| { + if let Some(mut diag) = signal.diagnostic() { + let category = diag.category().expect("linter diagnostic has no code"); + let severity = settings.get_current_settings().expect("project").get_severity_from_rule_code(category).expect( "If you see this error, it means you need to run cargo codegen-configuration", ); - for action in signal.actions() { - if !action.is_suppression() { - rule_has_code_action = true; - diag = diag.add_code_suggestion(action.into()); - } - } - - let error = diag - .with_severity(severity) - .with_file_path(file.clone()) - .with_file_source_code(code); - let res = write_diagnostic(code, error); - - // Abort the analysis on error - if let Err(err) = res { - return ControlFlow::Break(err); + for action in signal.actions() { + if !action.is_suppression() { + diag = diag.add_code_suggestion(action.into()); } } - ControlFlow::Continue(()) - }, - ); + let error = diag + .with_severity(severity) + .with_file_path(file) + .with_file_source_code(code); + let res = write_diagnostic(code, error); - // Result is Some(_) if analysis aborted with an error - for diagnostic in diagnostics { - write_diagnostic(code, diagnostic)?; - } - } - - if test.expect_diagnostic && rule_has_code_action && !has_fix_kind { - bail!("The rule '{}' emitted code actions via `action` function, but you didn't mark rule with `fix_kind`.", rule) - } + // Abort the analysis on error + if let Err(err) = res { + eprintln!("Error: {err}"); + return ControlFlow::Break(err); + } + } - if test.expect_diagnostic { - // Fail the test if the analysis didn't emit any diagnostic - ensure!( - diagnostic_count == 1, - "analysis of {}/{} returned no diagnostics.\n code snippet:\n {}", - group, - rule, - code - ); + ControlFlow::Continue(()) + }); } } BlockType::Json => { @@ -362,9 +314,7 @@ fn assert_lint( if parse.has_errors() { for diag in parse.into_diagnostics() { - let error = diag - .with_file_path(file.clone()) - .with_file_source_code(code); + let error = diag.with_file_path(file).with_file_source_code(code); write_diagnostic(code, error)?; } } else { @@ -377,48 +327,34 @@ fn assert_lint( }; let options = AnalyzerOptions::default(); - let (_, diagnostics) = biome_json_analyze::analyze( - &root, - filter, - &options, - |signal| { - if let Some(mut diag) = signal.diagnostic() { - let category = diag.category().expect("linter diagnostic has no code"); - let severity = settings.get_current_settings().expect("project").get_severity_from_rule_code(category).expect( + biome_json_analyze::analyze(&root, filter, &options, |signal| { + if let Some(mut diag) = signal.diagnostic() { + let category = diag.category().expect("linter diagnostic has no code"); + let severity = settings.get_current_settings().expect("project").get_severity_from_rule_code(category).expect( "If you see this error, it means you need to run cargo codegen-configuration", ); - for action in signal.actions() { - if !action.is_suppression() { - rule_has_code_action = true; - diag = diag.add_code_suggestion(action.into()); - } - } - - let error = diag - .with_severity(severity) - .with_file_path(file.clone()) - .with_file_source_code(code); - let res = write_diagnostic(code, error); - - // Abort the analysis on error - if let Err(err) = res { - return ControlFlow::Break(err); + for action in signal.actions() { + if !action.is_suppression() { + diag = diag.add_code_suggestion(action.into()); } } - ControlFlow::Continue(()) - }, - ); + let error = diag + .with_severity(severity) + .with_file_path(file) + .with_file_source_code(code); + let res = write_diagnostic(code, error); - // Result is Some(_) if analysis aborted with an error - for diagnostic in diagnostics { - write_diagnostic(code, diagnostic)?; - } + // Abort the analysis on error + if let Err(err) = res { + eprintln!("Error: {err}"); + return ControlFlow::Break(err); + } + } - if test.expect_diagnostic && rule_has_code_action && !has_fix_kind { - bail!("The rule '{}' emitted code actions via `action` function, but you didn't mark rule with `fix_kind`.", rule) - } + ControlFlow::Continue(()) + }); } } BlockType::Css => { @@ -426,9 +362,7 @@ fn assert_lint( if parse.has_errors() { for diag in parse.into_diagnostics() { - let error = diag - .with_file_path(file.clone()) - .with_file_source_code(code); + let error = diag.with_file_path(file).with_file_source_code(code); write_diagnostic(code, error)?; } } else { @@ -441,48 +375,34 @@ fn assert_lint( }; let options = AnalyzerOptions::default(); - let (_, diagnostics) = biome_css_analyze::analyze( - &root, - filter, - &options, - |signal| { - if let Some(mut diag) = signal.diagnostic() { - let category = diag.category().expect("linter diagnostic has no code"); - let severity = settings.get_current_settings().expect("project").get_severity_from_rule_code(category).expect( + biome_css_analyze::analyze(&root, filter, &options, |signal| { + if let Some(mut diag) = signal.diagnostic() { + let category = diag.category().expect("linter diagnostic has no code"); + let severity = settings.get_current_settings().expect("project").get_severity_from_rule_code(category).expect( "If you see this error, it means you need to run cargo codegen-configuration", ); - for action in signal.actions() { - if !action.is_suppression() { - rule_has_code_action = true; - diag = diag.add_code_suggestion(action.into()); - } - } - - let error = diag - .with_severity(severity) - .with_file_path(file.clone()) - .with_file_source_code(code); - let res = write_diagnostic(code, error); - - // Abort the analysis on error - if let Err(err) = res { - return ControlFlow::Break(err); + for action in signal.actions() { + if !action.is_suppression() { + diag = diag.add_code_suggestion(action.into()); } } - ControlFlow::Continue(()) - }, - ); + let error = diag + .with_severity(severity) + .with_file_path(file) + .with_file_source_code(code); + let res = write_diagnostic(code, error); - // Result is Some(_) if analysis aborted with an error - for diagnostic in diagnostics { - write_diagnostic(code, diagnostic)?; - } + // Abort the analysis on error + if let Err(err) = res { + eprintln!("Error: {err}"); + return ControlFlow::Break(err); + } + } - if test.expect_diagnostic && rule_has_code_action && !has_fix_kind { - bail!("The rule '{}' emitted code actions via `action` function, but you didn't mark rule with `fix_kind`.", rule) - } + ControlFlow::Continue(()) + }); } } // Foreign code blocks should be already ignored by tests @@ -491,16 +411,22 @@ fn assert_lint( } } + if test.expect_diagnostic { + // Fail the test if the analysis didn't emit any diagnostic + ensure!( + diagnostic_count == 1, + "Analysis of '{group}/{rule}' on the following code block returned no diagnostics.\n\n{code}", + ); + } + Ok(()) } -/// Parse the documentation fragment for a lint rule (in markdown) and generates -/// the content for the corresponding documentation page +/// Parse the documentation fragment for a lint rule (in markdown) and lint the code blcoks. fn parse_documentation( group: &'static str, rule: &'static str, docs: &'static str, - has_fix_kind: bool, ) -> anyhow::Result<()> { let parser = Parser::new(docs); @@ -517,7 +443,7 @@ fn parse_documentation( } Event::End(TagEnd::CodeBlock) => { if let Some((test, block)) = language.take() { - assert_lint(group, rule, &test, &block, has_fix_kind)?; + assert_lint(group, rule, &test, &block)?; } } Event::Text(text) => {