From 201891d22735570ac5aee6a5388e51199daf7a3a Mon Sep 17 00:00:00 2001 From: Maikel Date: Mon, 8 Dec 2025 10:32:53 +0100 Subject: [PATCH] feat(lint): implement noAmbiguousAnchorText --- .changeset/shy-sites-join.md | 11 + .../migrate/eslint_any_rule_to_biome.rs | 12 + .../src/analyzer/linter/rules.rs | 273 +++++----- .../src/categories.rs | 1 + crates/biome_html_analyze/src/a11y.rs | 22 + crates/biome_html_analyze/src/lib.rs | 1 + crates/biome_html_analyze/src/lint/nursery.rs | 3 +- .../lint/nursery/no_ambiguous_anchor_text.rs | 194 +++++++ .../noAmbiguousAnchorText/invalid.html | 49 ++ .../noAmbiguousAnchorText/invalid.html.snap | 465 ++++++++++++++++ .../nursery/noAmbiguousAnchorText/valid.html | 8 + .../noAmbiguousAnchorText/valid.html.snap | 16 + .../noAmbiguousAnchorText/words/invalid.html | 2 + .../words/invalid.html.snap | 26 + .../words/invalid.options.json | 17 + .../noAmbiguousAnchorText/words/valid.html | 2 + .../words/valid.html.snap | 10 + .../words/valid.options.json | 17 + crates/biome_html_syntax/src/element_ext.rs | 81 ++- crates/biome_js_analyze/src/lint/nursery.rs | 3 +- .../lint/nursery/no_ambiguous_anchor_text.rs | 194 +++++++ .../nursery/noAmbiguousAnchorText/invalid.jsx | 100 ++++ .../noAmbiguousAnchorText/invalid.jsx.snap | 510 ++++++++++++++++++ .../nursery/noAmbiguousAnchorText/valid.jsx | 20 + .../noAmbiguousAnchorText/valid.jsx.snap | 28 + .../noAmbiguousAnchorText/words/invalid.jsx | 4 + .../words/invalid.jsx.snap | 30 ++ .../words/invalid.options.json | 17 + .../noAmbiguousAnchorText/words/valid.jsx | 4 + .../words/valid.jsx.snap | 12 + .../words/valid.options.json | 17 + crates/biome_rule_options/src/lib.rs | 1 + .../src/no_ambiguous_anchor_text.rs | 25 + .../@biomejs/backend-jsonrpc/src/workspace.ts | 19 + .../@biomejs/biome/configuration_schema.json | 34 ++ 35 files changed, 2083 insertions(+), 145 deletions(-) create mode 100644 .changeset/shy-sites-join.md create mode 100644 crates/biome_html_analyze/src/a11y.rs create mode 100644 crates/biome_html_analyze/src/lint/nursery/no_ambiguous_anchor_text.rs create mode 100644 crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/invalid.html create mode 100644 crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/invalid.html.snap create mode 100644 crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/valid.html create mode 100644 crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/valid.html.snap create mode 100644 crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.html create mode 100644 crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.html.snap create mode 100644 crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.options.json create mode 100644 crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.html create mode 100644 crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.html.snap create mode 100644 crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.options.json create mode 100644 crates/biome_js_analyze/src/lint/nursery/no_ambiguous_anchor_text.rs create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/invalid.jsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/invalid.jsx.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/valid.jsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/valid.jsx.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.jsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.jsx.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.options.json create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.jsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.jsx.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.options.json create mode 100644 crates/biome_rule_options/src/no_ambiguous_anchor_text.rs diff --git a/.changeset/shy-sites-join.md b/.changeset/shy-sites-join.md new file mode 100644 index 000000000000..43e61dc8b298 --- /dev/null +++ b/.changeset/shy-sites-join.md @@ -0,0 +1,11 @@ +--- +"@biomejs/biome": patch +--- + +Added the nursery rule [`noAmbiguousAnchorText`](https://biomejs.dev/linter/rules/no-ambiguous-anchor-text/), which disallows ambiguous anchor descriptions. + +#### Invalid + +```html +learn more +``` diff --git a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs index 716706ff1210..3d5bbd0c59ee 100644 --- a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs +++ b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs @@ -1085,6 +1085,18 @@ pub(crate) fn migrate_eslint_any_rule( .get_or_insert(Default::default()); rule.set_level(rule.level().max(rule_severity.into())); } + "jsx-a11y/anchor-ambiguous-text" => { + if !options.include_nursery { + results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery); + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group + .unwrap_group_as_mut() + .no_ambiguous_anchor_text + .get_or_insert(Default::default()); + rule.set_level(rule.level().max(rule_severity.into())); + } "jsx-a11y/anchor-has-content" => { let group = rules.a11y.get_or_insert_with(Default::default); let rule = group diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index 3e1d311626f1..06c39d7689b7 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -93,6 +93,7 @@ pub enum RuleName { NoAccumulatingSpread, NoAdjacentSpacesInRegex, NoAlert, + NoAmbiguousAnchorText, NoApproximativeNumericConstant, NoArguments, NoAriaHiddenOnFocusable, @@ -491,6 +492,7 @@ impl RuleName { Self::NoAccumulatingSpread => "noAccumulatingSpread", Self::NoAdjacentSpacesInRegex => "noAdjacentSpacesInRegex", Self::NoAlert => "noAlert", + Self::NoAmbiguousAnchorText => "noAmbiguousAnchorText", Self::NoApproximativeNumericConstant => "noApproximativeNumericConstant", Self::NoArguments => "noArguments", Self::NoAriaHiddenOnFocusable => "noAriaHiddenOnFocusable", @@ -893,6 +895,7 @@ impl RuleName { Self::NoAccumulatingSpread => RuleGroup::Performance, Self::NoAdjacentSpacesInRegex => RuleGroup::Complexity, Self::NoAlert => RuleGroup::Suspicious, + Self::NoAmbiguousAnchorText => RuleGroup::Nursery, Self::NoApproximativeNumericConstant => RuleGroup::Suspicious, Self::NoArguments => RuleGroup::Complexity, Self::NoAriaHiddenOnFocusable => RuleGroup::A11y, @@ -1294,6 +1297,7 @@ impl std::str::FromStr for RuleName { "noAccumulatingSpread" => Ok(Self::NoAccumulatingSpread), "noAdjacentSpacesInRegex" => Ok(Self::NoAdjacentSpacesInRegex), "noAlert" => Ok(Self::NoAlert), + "noAmbiguousAnchorText" => Ok(Self::NoAmbiguousAnchorText), "noApproximativeNumericConstant" => Ok(Self::NoApproximativeNumericConstant), "noArguments" => Ok(Self::NoArguments), "noAriaHiddenOnFocusable" => Ok(Self::NoAriaHiddenOnFocusable), @@ -4824,10 +4828,11 @@ impl From for Correctness { #[cfg_attr(feature = "schema", derive(JsonSchema))] #[serde(rename_all = "camelCase", default, deny_unknown_fields)] #[doc = r" A list of rules that belong to this group"] -pub struct Nursery { # [doc = r" Enables the recommended rules for this group"] # [serde (skip_serializing_if = "Option::is_none")] pub recommended : Option < bool > , # [doc = "Disallow continue statements.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_continue : Option < RuleConfiguration < biome_rule_options :: no_continue :: NoContinueOptions >> , # [doc = "Restrict imports of deprecated exports.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_deprecated_imports : Option < RuleConfiguration < biome_rule_options :: no_deprecated_imports :: NoDeprecatedImportsOptions >> , # [doc = "Prevent the listing of duplicate dependencies. The rule supports the following dependency groups: \"bundledDependencies\", \"bundleDependencies\", \"dependencies\", \"devDependencies\", \"overrides\", \"optionalDependencies\", and \"peerDependencies\".\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_duplicate_dependencies : Option < RuleConfiguration < biome_rule_options :: no_duplicate_dependencies :: NoDuplicateDependenciesOptions >> , # [doc = "Disallow JSX prop spreading the same identifier multiple times.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_duplicated_spread_props : Option < RuleConfiguration < biome_rule_options :: no_duplicated_spread_props :: NoDuplicatedSpreadPropsOptions >> , # [doc = "Disallow empty sources.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_empty_source : Option < RuleConfiguration < biome_rule_options :: no_empty_source :: NoEmptySourceOptions >> , # [doc = "Require the use of === or !== for comparison with null.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_equals_to_null : Option < RuleFixConfiguration < biome_rule_options :: no_equals_to_null :: NoEqualsToNullOptions >> , # [doc = "Require Promise-like statements to be handled appropriately.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_floating_promises : Option < RuleFixConfiguration < biome_rule_options :: no_floating_promises :: NoFloatingPromisesOptions >> , # [doc = "Disallow iterating using a for-in loop.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_for_in : Option < RuleConfiguration < biome_rule_options :: no_for_in :: NoForInOptions >> , # [doc = "Prevent import cycles.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_import_cycles : Option < RuleConfiguration < biome_rule_options :: no_import_cycles :: NoImportCyclesOptions >> , # [doc = "Disallows the usage of the unary operators ++ and --.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_increment_decrement : Option < RuleConfiguration < biome_rule_options :: no_increment_decrement :: NoIncrementDecrementOptions >> , # [doc = "Disallow string literals inside JSX elements.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_jsx_literals : Option < RuleConfiguration < biome_rule_options :: no_jsx_literals :: NoJsxLiteralsOptions >> , # [doc = "Prevent problematic leaked values from being rendered.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_leaked_render : Option < RuleConfiguration < biome_rule_options :: no_leaked_render :: NoLeakedRenderOptions >> , # [doc = "Disallow Promises to be used in places where they are almost certainly a mistake.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_misused_promises : Option < RuleFixConfiguration < biome_rule_options :: no_misused_promises :: NoMisusedPromisesOptions >> , # [doc = "Disallow creating multiline strings by escaping newlines.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_multi_str : Option < RuleConfiguration < biome_rule_options :: no_multi_str :: NoMultiStrOptions >> , # [doc = "Prevent client components from being async functions.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_next_async_client_component : Option < RuleConfiguration < biome_rule_options :: no_next_async_client_component :: NoNextAsyncClientComponentOptions >> , # [doc = "Disallow function parameters that are only used in recursive calls.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_parameters_only_used_in_recursion : Option < RuleFixConfiguration < biome_rule_options :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursionOptions >> , # [doc = "Disallow the use of the __proto__ property.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_proto : Option < RuleConfiguration < biome_rule_options :: no_proto :: NoProtoOptions >> , # [doc = "Replaces usages of forwardRef with passing ref as a prop.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_react_forward_ref : Option < RuleFixConfiguration < biome_rule_options :: no_react_forward_ref :: NoReactForwardRefOptions >> , # [doc = "Disallow javascript: URLs in HTML.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_script_url : Option < RuleConfiguration < biome_rule_options :: no_script_url :: NoScriptUrlOptions >> , # [doc = "Disallow variable declarations from shadowing variables declared in the outer scope.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_shadow : Option < RuleConfiguration < biome_rule_options :: no_shadow :: NoShadowOptions >> , # [doc = "Prevent the usage of synchronous scripts.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_sync_scripts : Option < RuleConfiguration < biome_rule_options :: no_sync_scripts :: NoSyncScriptsOptions >> , # [doc = "Disallow ternary operators.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_ternary : Option < RuleConfiguration < biome_rule_options :: no_ternary :: NoTernaryOptions >> , # [doc = "Disallow unknown DOM properties.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_unknown_attribute : Option < RuleConfiguration < biome_rule_options :: no_unknown_attribute :: NoUnknownAttributeOptions >> , # [doc = "Disallow unnecessary type-based conditions that can be statically determined as redundant.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_unnecessary_conditions : Option < RuleConfiguration < biome_rule_options :: no_unnecessary_conditions :: NoUnnecessaryConditionsOptions >> , # [doc = "Warn when importing non-existing exports.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_unresolved_imports : Option < RuleConfiguration < biome_rule_options :: no_unresolved_imports :: NoUnresolvedImportsOptions >> , # [doc = "Disallow expression statements that are neither a function call nor an assignment.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_unused_expressions : Option < RuleConfiguration < biome_rule_options :: no_unused_expressions :: NoUnusedExpressionsOptions >> , # [doc = "Disallow unused catch bindings.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_useless_catch_binding : Option < RuleFixConfiguration < biome_rule_options :: no_useless_catch_binding :: NoUselessCatchBindingOptions >> , # [doc = "Disallow the use of useless undefined.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_useless_undefined : Option < RuleFixConfiguration < biome_rule_options :: no_useless_undefined :: NoUselessUndefinedOptions >> , # [doc = "Enforce that Vue component data options are declared as functions.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_data_object_declaration : Option < RuleFixConfiguration < biome_rule_options :: no_vue_data_object_declaration :: NoVueDataObjectDeclarationOptions >> , # [doc = "Disallow duplicate keys in Vue component data, methods, computed properties, and other options.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_duplicate_keys : Option < RuleConfiguration < biome_rule_options :: no_vue_duplicate_keys :: NoVueDuplicateKeysOptions >> , # [doc = "Disallow reserved keys in Vue component data and computed properties.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_keys : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_keys :: NoVueReservedKeysOptions >> , # [doc = "Disallow reserved names to be used as props.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_props : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_props :: NoVueReservedPropsOptions >> , # [doc = "Disallow destructuring of props passed to setup in Vue projects.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_setup_props_reactivity_loss : Option < RuleConfiguration < biome_rule_options :: no_vue_setup_props_reactivity_loss :: NoVueSetupPropsReactivityLossOptions >> , # [doc = "Disallow using v-if and v-for directives on the same element.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_v_if_with_v_for : Option < RuleConfiguration < biome_rule_options :: no_vue_v_if_with_v_for :: NoVueVIfWithVForOptions >> , # [doc = "Require Array#sort and Array#toSorted calls to always provide a compareFunction.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_array_sort_compare : Option < RuleConfiguration < biome_rule_options :: use_array_sort_compare :: UseArraySortCompareOptions >> , # [doc = "Enforce that await is only used on Promise values.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_await_thenable : Option < RuleConfiguration < biome_rule_options :: use_await_thenable :: UseAwaitThenableOptions >> , # [doc = "Enforce consistent arrow function bodies.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_consistent_arrow_return : Option < RuleFixConfiguration < biome_rule_options :: use_consistent_arrow_return :: UseConsistentArrowReturnOptions >> , # [doc = "Require all descriptions to follow the same style (either block or inline) to maintain consistency and improve readability across the schema.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_consistent_graphql_descriptions : Option < RuleConfiguration < biome_rule_options :: use_consistent_graphql_descriptions :: UseConsistentGraphqlDescriptionsOptions >> , # [doc = "Require the @deprecated directive to specify a deletion date.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_deprecated_date : Option < RuleConfiguration < biome_rule_options :: use_deprecated_date :: UseDeprecatedDateOptions >> , # [doc = "Require destructuring from arrays and/or objects.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_destructuring : Option < RuleConfiguration < biome_rule_options :: use_destructuring :: UseDestructuringOptions >> , # [doc = "Require switch-case statements to be exhaustive.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_exhaustive_switch_cases : Option < RuleFixConfiguration < biome_rule_options :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCasesOptions >> , # [doc = "Enforce types in functions, methods, variables, and parameters.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_explicit_type : Option < RuleConfiguration < biome_rule_options :: use_explicit_type :: UseExplicitTypeOptions >> , # [doc = "Enforce the use of Array.prototype.find() over Array.prototype.filter() followed by [0] when looking for a single result.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_find : Option < RuleConfiguration < biome_rule_options :: use_find :: UseFindOptions >> , # [doc = "Enforce a maximum number of parameters in function definitions.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_max_params : Option < RuleConfiguration < biome_rule_options :: use_max_params :: UseMaxParamsOptions >> , # [doc = "Disallow use* hooks outside of component$ or other use* hooks in Qwik applications.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_qwik_method_usage : Option < RuleConfiguration < biome_rule_options :: use_qwik_method_usage :: UseQwikMethodUsageOptions >> , # [doc = "Disallow unserializable expressions in Qwik dollar ($) scopes.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_qwik_valid_lexical_scope : Option < RuleConfiguration < biome_rule_options :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScopeOptions >> , # [doc = "Enforce RegExp#exec over String#match if no global flag is provided.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_regexp_exec : Option < RuleConfiguration < biome_rule_options :: use_regexp_exec :: UseRegexpExecOptions >> , # [doc = "Enforce the presence of required scripts in package.json.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_required_scripts : Option < RuleConfiguration < biome_rule_options :: use_required_scripts :: UseRequiredScriptsOptions >> , # [doc = "Enforce the sorting of CSS utility classes.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_sorted_classes : Option < RuleFixConfiguration < biome_rule_options :: use_sorted_classes :: UseSortedClassesOptions >> , # [doc = "Enforce the use of the spread operator over .apply().\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_spread : Option < RuleFixConfiguration < biome_rule_options :: use_spread :: UseSpreadOptions >> , # [doc = "Enforce unique operation names across a GraphQL document.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_unique_graphql_operation_name : Option < RuleConfiguration < biome_rule_options :: use_unique_graphql_operation_name :: UseUniqueGraphqlOperationNameOptions >> , # [doc = "Enforce specific order of Vue compiler macros.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_define_macros_order : Option < RuleFixConfiguration < biome_rule_options :: use_vue_define_macros_order :: UseVueDefineMacrosOrderOptions >> , # [doc = "Enforce hyphenated (kebab-case) attribute names in Vue templates.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_hyphenated_attributes : Option < RuleFixConfiguration < biome_rule_options :: use_vue_hyphenated_attributes :: UseVueHyphenatedAttributesOptions >> , # [doc = "Enforce multi-word component names in Vue components.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_multi_word_component_names : Option < RuleConfiguration < biome_rule_options :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNamesOptions >> , # [doc = "Forbids v-bind directives with missing arguments or invalid modifiers.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_bind : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_bind :: UseVueValidVBindOptions >> , # [doc = "Enforce valid usage of v-else.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_else : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_else :: UseVueValidVElseOptions >> , # [doc = "Enforce valid v-else-if directives.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_else_if : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_else_if :: UseVueValidVElseIfOptions >> , # [doc = "Enforce valid v-html directives.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_html : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_html :: UseVueValidVHtmlOptions >> , # [doc = "Enforces valid v-if usage for Vue templates.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_if : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_if :: UseVueValidVIfOptions >> , # [doc = "Enforce valid v-on directives with proper arguments, modifiers, and handlers.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_on : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_on :: UseVueValidVOnOptions >> , # [doc = "Enforce valid v-text Vue directives.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_text : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_text :: UseVueValidVTextOptions >> } +pub struct Nursery { # [doc = r" Enables the recommended rules for this group"] # [serde (skip_serializing_if = "Option::is_none")] pub recommended : Option < bool > , # [doc = "Disallow ambiguous anchor descriptions.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_ambiguous_anchor_text : Option < RuleConfiguration < biome_rule_options :: no_ambiguous_anchor_text :: NoAmbiguousAnchorTextOptions >> , # [doc = "Disallow continue statements.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_continue : Option < RuleConfiguration < biome_rule_options :: no_continue :: NoContinueOptions >> , # [doc = "Restrict imports of deprecated exports.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_deprecated_imports : Option < RuleConfiguration < biome_rule_options :: no_deprecated_imports :: NoDeprecatedImportsOptions >> , # [doc = "Prevent the listing of duplicate dependencies. The rule supports the following dependency groups: \"bundledDependencies\", \"bundleDependencies\", \"dependencies\", \"devDependencies\", \"overrides\", \"optionalDependencies\", and \"peerDependencies\".\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_duplicate_dependencies : Option < RuleConfiguration < biome_rule_options :: no_duplicate_dependencies :: NoDuplicateDependenciesOptions >> , # [doc = "Disallow JSX prop spreading the same identifier multiple times.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_duplicated_spread_props : Option < RuleConfiguration < biome_rule_options :: no_duplicated_spread_props :: NoDuplicatedSpreadPropsOptions >> , # [doc = "Disallow empty sources.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_empty_source : Option < RuleConfiguration < biome_rule_options :: no_empty_source :: NoEmptySourceOptions >> , # [doc = "Require the use of === or !== for comparison with null.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_equals_to_null : Option < RuleFixConfiguration < biome_rule_options :: no_equals_to_null :: NoEqualsToNullOptions >> , # [doc = "Require Promise-like statements to be handled appropriately.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_floating_promises : Option < RuleFixConfiguration < biome_rule_options :: no_floating_promises :: NoFloatingPromisesOptions >> , # [doc = "Disallow iterating using a for-in loop.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_for_in : Option < RuleConfiguration < biome_rule_options :: no_for_in :: NoForInOptions >> , # [doc = "Prevent import cycles.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_import_cycles : Option < RuleConfiguration < biome_rule_options :: no_import_cycles :: NoImportCyclesOptions >> , # [doc = "Disallows the usage of the unary operators ++ and --.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_increment_decrement : Option < RuleConfiguration < biome_rule_options :: no_increment_decrement :: NoIncrementDecrementOptions >> , # [doc = "Disallow string literals inside JSX elements.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_jsx_literals : Option < RuleConfiguration < biome_rule_options :: no_jsx_literals :: NoJsxLiteralsOptions >> , # [doc = "Prevent problematic leaked values from being rendered.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_leaked_render : Option < RuleConfiguration < biome_rule_options :: no_leaked_render :: NoLeakedRenderOptions >> , # [doc = "Disallow Promises to be used in places where they are almost certainly a mistake.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_misused_promises : Option < RuleFixConfiguration < biome_rule_options :: no_misused_promises :: NoMisusedPromisesOptions >> , # [doc = "Disallow creating multiline strings by escaping newlines.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_multi_str : Option < RuleConfiguration < biome_rule_options :: no_multi_str :: NoMultiStrOptions >> , # [doc = "Prevent client components from being async functions.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_next_async_client_component : Option < RuleConfiguration < biome_rule_options :: no_next_async_client_component :: NoNextAsyncClientComponentOptions >> , # [doc = "Disallow function parameters that are only used in recursive calls.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_parameters_only_used_in_recursion : Option < RuleFixConfiguration < biome_rule_options :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursionOptions >> , # [doc = "Disallow the use of the __proto__ property.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_proto : Option < RuleConfiguration < biome_rule_options :: no_proto :: NoProtoOptions >> , # [doc = "Replaces usages of forwardRef with passing ref as a prop.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_react_forward_ref : Option < RuleFixConfiguration < biome_rule_options :: no_react_forward_ref :: NoReactForwardRefOptions >> , # [doc = "Disallow javascript: URLs in HTML.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_script_url : Option < RuleConfiguration < biome_rule_options :: no_script_url :: NoScriptUrlOptions >> , # [doc = "Disallow variable declarations from shadowing variables declared in the outer scope.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_shadow : Option < RuleConfiguration < biome_rule_options :: no_shadow :: NoShadowOptions >> , # [doc = "Prevent the usage of synchronous scripts.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_sync_scripts : Option < RuleConfiguration < biome_rule_options :: no_sync_scripts :: NoSyncScriptsOptions >> , # [doc = "Disallow ternary operators.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_ternary : Option < RuleConfiguration < biome_rule_options :: no_ternary :: NoTernaryOptions >> , # [doc = "Disallow unknown DOM properties.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_unknown_attribute : Option < RuleConfiguration < biome_rule_options :: no_unknown_attribute :: NoUnknownAttributeOptions >> , # [doc = "Disallow unnecessary type-based conditions that can be statically determined as redundant.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_unnecessary_conditions : Option < RuleConfiguration < biome_rule_options :: no_unnecessary_conditions :: NoUnnecessaryConditionsOptions >> , # [doc = "Warn when importing non-existing exports.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_unresolved_imports : Option < RuleConfiguration < biome_rule_options :: no_unresolved_imports :: NoUnresolvedImportsOptions >> , # [doc = "Disallow expression statements that are neither a function call nor an assignment.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_unused_expressions : Option < RuleConfiguration < biome_rule_options :: no_unused_expressions :: NoUnusedExpressionsOptions >> , # [doc = "Disallow unused catch bindings.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_useless_catch_binding : Option < RuleFixConfiguration < biome_rule_options :: no_useless_catch_binding :: NoUselessCatchBindingOptions >> , # [doc = "Disallow the use of useless undefined.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_useless_undefined : Option < RuleFixConfiguration < biome_rule_options :: no_useless_undefined :: NoUselessUndefinedOptions >> , # [doc = "Enforce that Vue component data options are declared as functions.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_data_object_declaration : Option < RuleFixConfiguration < biome_rule_options :: no_vue_data_object_declaration :: NoVueDataObjectDeclarationOptions >> , # [doc = "Disallow duplicate keys in Vue component data, methods, computed properties, and other options.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_duplicate_keys : Option < RuleConfiguration < biome_rule_options :: no_vue_duplicate_keys :: NoVueDuplicateKeysOptions >> , # [doc = "Disallow reserved keys in Vue component data and computed properties.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_keys : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_keys :: NoVueReservedKeysOptions >> , # [doc = "Disallow reserved names to be used as props.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_props : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_props :: NoVueReservedPropsOptions >> , # [doc = "Disallow destructuring of props passed to setup in Vue projects.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_setup_props_reactivity_loss : Option < RuleConfiguration < biome_rule_options :: no_vue_setup_props_reactivity_loss :: NoVueSetupPropsReactivityLossOptions >> , # [doc = "Disallow using v-if and v-for directives on the same element.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_v_if_with_v_for : Option < RuleConfiguration < biome_rule_options :: no_vue_v_if_with_v_for :: NoVueVIfWithVForOptions >> , # [doc = "Require Array#sort and Array#toSorted calls to always provide a compareFunction.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_array_sort_compare : Option < RuleConfiguration < biome_rule_options :: use_array_sort_compare :: UseArraySortCompareOptions >> , # [doc = "Enforce that await is only used on Promise values.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_await_thenable : Option < RuleConfiguration < biome_rule_options :: use_await_thenable :: UseAwaitThenableOptions >> , # [doc = "Enforce consistent arrow function bodies.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_consistent_arrow_return : Option < RuleFixConfiguration < biome_rule_options :: use_consistent_arrow_return :: UseConsistentArrowReturnOptions >> , # [doc = "Require all descriptions to follow the same style (either block or inline) to maintain consistency and improve readability across the schema.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_consistent_graphql_descriptions : Option < RuleConfiguration < biome_rule_options :: use_consistent_graphql_descriptions :: UseConsistentGraphqlDescriptionsOptions >> , # [doc = "Require the @deprecated directive to specify a deletion date.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_deprecated_date : Option < RuleConfiguration < biome_rule_options :: use_deprecated_date :: UseDeprecatedDateOptions >> , # [doc = "Require destructuring from arrays and/or objects.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_destructuring : Option < RuleConfiguration < biome_rule_options :: use_destructuring :: UseDestructuringOptions >> , # [doc = "Require switch-case statements to be exhaustive.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_exhaustive_switch_cases : Option < RuleFixConfiguration < biome_rule_options :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCasesOptions >> , # [doc = "Enforce types in functions, methods, variables, and parameters.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_explicit_type : Option < RuleConfiguration < biome_rule_options :: use_explicit_type :: UseExplicitTypeOptions >> , # [doc = "Enforce the use of Array.prototype.find() over Array.prototype.filter() followed by [0] when looking for a single result.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_find : Option < RuleConfiguration < biome_rule_options :: use_find :: UseFindOptions >> , # [doc = "Enforce a maximum number of parameters in function definitions.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_max_params : Option < RuleConfiguration < biome_rule_options :: use_max_params :: UseMaxParamsOptions >> , # [doc = "Disallow use* hooks outside of component$ or other use* hooks in Qwik applications.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_qwik_method_usage : Option < RuleConfiguration < biome_rule_options :: use_qwik_method_usage :: UseQwikMethodUsageOptions >> , # [doc = "Disallow unserializable expressions in Qwik dollar ($) scopes.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_qwik_valid_lexical_scope : Option < RuleConfiguration < biome_rule_options :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScopeOptions >> , # [doc = "Enforce RegExp#exec over String#match if no global flag is provided.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_regexp_exec : Option < RuleConfiguration < biome_rule_options :: use_regexp_exec :: UseRegexpExecOptions >> , # [doc = "Enforce the presence of required scripts in package.json.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_required_scripts : Option < RuleConfiguration < biome_rule_options :: use_required_scripts :: UseRequiredScriptsOptions >> , # [doc = "Enforce the sorting of CSS utility classes.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_sorted_classes : Option < RuleFixConfiguration < biome_rule_options :: use_sorted_classes :: UseSortedClassesOptions >> , # [doc = "Enforce the use of the spread operator over .apply().\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_spread : Option < RuleFixConfiguration < biome_rule_options :: use_spread :: UseSpreadOptions >> , # [doc = "Enforce unique operation names across a GraphQL document.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_unique_graphql_operation_name : Option < RuleConfiguration < biome_rule_options :: use_unique_graphql_operation_name :: UseUniqueGraphqlOperationNameOptions >> , # [doc = "Enforce specific order of Vue compiler macros.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_define_macros_order : Option < RuleFixConfiguration < biome_rule_options :: use_vue_define_macros_order :: UseVueDefineMacrosOrderOptions >> , # [doc = "Enforce hyphenated (kebab-case) attribute names in Vue templates.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_hyphenated_attributes : Option < RuleFixConfiguration < biome_rule_options :: use_vue_hyphenated_attributes :: UseVueHyphenatedAttributesOptions >> , # [doc = "Enforce multi-word component names in Vue components.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_multi_word_component_names : Option < RuleConfiguration < biome_rule_options :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNamesOptions >> , # [doc = "Forbids v-bind directives with missing arguments or invalid modifiers.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_bind : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_bind :: UseVueValidVBindOptions >> , # [doc = "Enforce valid usage of v-else.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_else : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_else :: UseVueValidVElseOptions >> , # [doc = "Enforce valid v-else-if directives.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_else_if : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_else_if :: UseVueValidVElseIfOptions >> , # [doc = "Enforce valid v-html directives.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_html : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_html :: UseVueValidVHtmlOptions >> , # [doc = "Enforces valid v-if usage for Vue templates.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_if : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_if :: UseVueValidVIfOptions >> , # [doc = "Enforce valid v-on directives with proper arguments, modifiers, and handlers.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_on : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_on :: UseVueValidVOnOptions >> , # [doc = "Enforce valid v-text Vue directives.\nSee "] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_text : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_text :: UseVueValidVTextOptions >> } impl Nursery { const GROUP_NAME: &'static str = "nursery"; pub(crate) const GROUP_RULES: &'static [&'static str] = &[ + "noAmbiguousAnchorText", "noContinue", "noDeprecatedImports", "noDuplicateDependencies", @@ -4891,9 +4896,9 @@ impl Nursery { "useVueValidVText", ]; const RECOMMENDED_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -4957,6 +4962,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[58]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[59]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[60]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[61]), ]; } impl RuleGroupExt for Nursery { @@ -4968,620 +4974,630 @@ impl RuleGroupExt for Nursery { } fn get_enabled_rules(&self) -> FxHashSet> { let mut index_set = FxHashSet::default(); - if let Some(rule) = self.no_continue.as_ref() + if let Some(rule) = self.no_ambiguous_anchor_text.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0])); } - if let Some(rule) = self.no_deprecated_imports.as_ref() + if let Some(rule) = self.no_continue.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1])); } - if let Some(rule) = self.no_duplicate_dependencies.as_ref() + if let Some(rule) = self.no_deprecated_imports.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2])); } - if let Some(rule) = self.no_duplicated_spread_props.as_ref() + if let Some(rule) = self.no_duplicate_dependencies.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3])); } - if let Some(rule) = self.no_empty_source.as_ref() + if let Some(rule) = self.no_duplicated_spread_props.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4])); } - if let Some(rule) = self.no_equals_to_null.as_ref() + if let Some(rule) = self.no_empty_source.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5])); } - if let Some(rule) = self.no_floating_promises.as_ref() + if let Some(rule) = self.no_equals_to_null.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6])); } - if let Some(rule) = self.no_for_in.as_ref() + if let Some(rule) = self.no_floating_promises.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); } - if let Some(rule) = self.no_import_cycles.as_ref() + if let Some(rule) = self.no_for_in.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } - if let Some(rule) = self.no_increment_decrement.as_ref() + if let Some(rule) = self.no_import_cycles.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } - if let Some(rule) = self.no_jsx_literals.as_ref() + if let Some(rule) = self.no_increment_decrement.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } - if let Some(rule) = self.no_leaked_render.as_ref() + if let Some(rule) = self.no_jsx_literals.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } - if let Some(rule) = self.no_misused_promises.as_ref() + if let Some(rule) = self.no_leaked_render.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } - if let Some(rule) = self.no_multi_str.as_ref() + if let Some(rule) = self.no_misused_promises.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } - if let Some(rule) = self.no_next_async_client_component.as_ref() + if let Some(rule) = self.no_multi_str.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } - if let Some(rule) = self.no_parameters_only_used_in_recursion.as_ref() + if let Some(rule) = self.no_next_async_client_component.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } - if let Some(rule) = self.no_proto.as_ref() + if let Some(rule) = self.no_parameters_only_used_in_recursion.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } - if let Some(rule) = self.no_react_forward_ref.as_ref() + if let Some(rule) = self.no_proto.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } - if let Some(rule) = self.no_script_url.as_ref() + if let Some(rule) = self.no_react_forward_ref.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } - if let Some(rule) = self.no_shadow.as_ref() + if let Some(rule) = self.no_script_url.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } - if let Some(rule) = self.no_sync_scripts.as_ref() + if let Some(rule) = self.no_shadow.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } - if let Some(rule) = self.no_ternary.as_ref() + if let Some(rule) = self.no_sync_scripts.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } - if let Some(rule) = self.no_unknown_attribute.as_ref() + if let Some(rule) = self.no_ternary.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } - if let Some(rule) = self.no_unnecessary_conditions.as_ref() + if let Some(rule) = self.no_unknown_attribute.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } - if let Some(rule) = self.no_unresolved_imports.as_ref() + if let Some(rule) = self.no_unnecessary_conditions.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } - if let Some(rule) = self.no_unused_expressions.as_ref() + if let Some(rule) = self.no_unresolved_imports.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); } - if let Some(rule) = self.no_useless_catch_binding.as_ref() + if let Some(rule) = self.no_unused_expressions.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } - if let Some(rule) = self.no_useless_undefined.as_ref() + if let Some(rule) = self.no_useless_catch_binding.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } - if let Some(rule) = self.no_vue_data_object_declaration.as_ref() + if let Some(rule) = self.no_useless_undefined.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } - if let Some(rule) = self.no_vue_duplicate_keys.as_ref() + if let Some(rule) = self.no_vue_data_object_declaration.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } - if let Some(rule) = self.no_vue_reserved_keys.as_ref() + if let Some(rule) = self.no_vue_duplicate_keys.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } - if let Some(rule) = self.no_vue_reserved_props.as_ref() + if let Some(rule) = self.no_vue_reserved_keys.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } - if let Some(rule) = self.no_vue_setup_props_reactivity_loss.as_ref() + if let Some(rule) = self.no_vue_reserved_props.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } - if let Some(rule) = self.no_vue_v_if_with_v_for.as_ref() + if let Some(rule) = self.no_vue_setup_props_reactivity_loss.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } - if let Some(rule) = self.use_array_sort_compare.as_ref() + if let Some(rule) = self.no_vue_v_if_with_v_for.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } - if let Some(rule) = self.use_await_thenable.as_ref() + if let Some(rule) = self.use_array_sort_compare.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } - if let Some(rule) = self.use_consistent_arrow_return.as_ref() + if let Some(rule) = self.use_await_thenable.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36])); } - if let Some(rule) = self.use_consistent_graphql_descriptions.as_ref() + if let Some(rule) = self.use_consistent_arrow_return.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); } - if let Some(rule) = self.use_deprecated_date.as_ref() + if let Some(rule) = self.use_consistent_graphql_descriptions.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])); } - if let Some(rule) = self.use_destructuring.as_ref() + if let Some(rule) = self.use_deprecated_date.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); } - if let Some(rule) = self.use_exhaustive_switch_cases.as_ref() + if let Some(rule) = self.use_destructuring.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40])); } - if let Some(rule) = self.use_explicit_type.as_ref() + if let Some(rule) = self.use_exhaustive_switch_cases.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41])); } - if let Some(rule) = self.use_find.as_ref() + if let Some(rule) = self.use_explicit_type.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42])); } - if let Some(rule) = self.use_max_params.as_ref() + if let Some(rule) = self.use_find.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43])); } - if let Some(rule) = self.use_qwik_method_usage.as_ref() + if let Some(rule) = self.use_max_params.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44])); } - if let Some(rule) = self.use_qwik_valid_lexical_scope.as_ref() + if let Some(rule) = self.use_qwik_method_usage.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45])); } - if let Some(rule) = self.use_regexp_exec.as_ref() + if let Some(rule) = self.use_qwik_valid_lexical_scope.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46])); } - if let Some(rule) = self.use_required_scripts.as_ref() + if let Some(rule) = self.use_regexp_exec.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47])); } - if let Some(rule) = self.use_sorted_classes.as_ref() + if let Some(rule) = self.use_required_scripts.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48])); } - if let Some(rule) = self.use_spread.as_ref() + if let Some(rule) = self.use_sorted_classes.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49])); } - if let Some(rule) = self.use_unique_graphql_operation_name.as_ref() + if let Some(rule) = self.use_spread.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50])); } - if let Some(rule) = self.use_vue_define_macros_order.as_ref() + if let Some(rule) = self.use_unique_graphql_operation_name.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51])); } - if let Some(rule) = self.use_vue_hyphenated_attributes.as_ref() + if let Some(rule) = self.use_vue_define_macros_order.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52])); } - if let Some(rule) = self.use_vue_multi_word_component_names.as_ref() + if let Some(rule) = self.use_vue_hyphenated_attributes.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53])); } - if let Some(rule) = self.use_vue_valid_v_bind.as_ref() + if let Some(rule) = self.use_vue_multi_word_component_names.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[54])); } - if let Some(rule) = self.use_vue_valid_v_else.as_ref() + if let Some(rule) = self.use_vue_valid_v_bind.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[55])); } - if let Some(rule) = self.use_vue_valid_v_else_if.as_ref() + if let Some(rule) = self.use_vue_valid_v_else.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[56])); } - if let Some(rule) = self.use_vue_valid_v_html.as_ref() + if let Some(rule) = self.use_vue_valid_v_else_if.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57])); } - if let Some(rule) = self.use_vue_valid_v_if.as_ref() + if let Some(rule) = self.use_vue_valid_v_html.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[58])); } - if let Some(rule) = self.use_vue_valid_v_on.as_ref() + if let Some(rule) = self.use_vue_valid_v_if.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[59])); } - if let Some(rule) = self.use_vue_valid_v_text.as_ref() + if let Some(rule) = self.use_vue_valid_v_on.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[60])); } + if let Some(rule) = self.use_vue_valid_v_text.as_ref() + && rule.is_enabled() + { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[61])); + } index_set } fn get_disabled_rules(&self) -> FxHashSet> { let mut index_set = FxHashSet::default(); - if let Some(rule) = self.no_continue.as_ref() + if let Some(rule) = self.no_ambiguous_anchor_text.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0])); } - if let Some(rule) = self.no_deprecated_imports.as_ref() + if let Some(rule) = self.no_continue.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1])); } - if let Some(rule) = self.no_duplicate_dependencies.as_ref() + if let Some(rule) = self.no_deprecated_imports.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2])); } - if let Some(rule) = self.no_duplicated_spread_props.as_ref() + if let Some(rule) = self.no_duplicate_dependencies.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3])); } - if let Some(rule) = self.no_empty_source.as_ref() + if let Some(rule) = self.no_duplicated_spread_props.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4])); } - if let Some(rule) = self.no_equals_to_null.as_ref() + if let Some(rule) = self.no_empty_source.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5])); } - if let Some(rule) = self.no_floating_promises.as_ref() + if let Some(rule) = self.no_equals_to_null.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6])); } - if let Some(rule) = self.no_for_in.as_ref() + if let Some(rule) = self.no_floating_promises.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); } - if let Some(rule) = self.no_import_cycles.as_ref() + if let Some(rule) = self.no_for_in.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } - if let Some(rule) = self.no_increment_decrement.as_ref() + if let Some(rule) = self.no_import_cycles.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } - if let Some(rule) = self.no_jsx_literals.as_ref() + if let Some(rule) = self.no_increment_decrement.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } - if let Some(rule) = self.no_leaked_render.as_ref() + if let Some(rule) = self.no_jsx_literals.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } - if let Some(rule) = self.no_misused_promises.as_ref() + if let Some(rule) = self.no_leaked_render.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } - if let Some(rule) = self.no_multi_str.as_ref() + if let Some(rule) = self.no_misused_promises.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } - if let Some(rule) = self.no_next_async_client_component.as_ref() + if let Some(rule) = self.no_multi_str.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } - if let Some(rule) = self.no_parameters_only_used_in_recursion.as_ref() + if let Some(rule) = self.no_next_async_client_component.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } - if let Some(rule) = self.no_proto.as_ref() + if let Some(rule) = self.no_parameters_only_used_in_recursion.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } - if let Some(rule) = self.no_react_forward_ref.as_ref() + if let Some(rule) = self.no_proto.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } - if let Some(rule) = self.no_script_url.as_ref() + if let Some(rule) = self.no_react_forward_ref.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } - if let Some(rule) = self.no_shadow.as_ref() + if let Some(rule) = self.no_script_url.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } - if let Some(rule) = self.no_sync_scripts.as_ref() + if let Some(rule) = self.no_shadow.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } - if let Some(rule) = self.no_ternary.as_ref() + if let Some(rule) = self.no_sync_scripts.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } - if let Some(rule) = self.no_unknown_attribute.as_ref() + if let Some(rule) = self.no_ternary.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } - if let Some(rule) = self.no_unnecessary_conditions.as_ref() + if let Some(rule) = self.no_unknown_attribute.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } - if let Some(rule) = self.no_unresolved_imports.as_ref() + if let Some(rule) = self.no_unnecessary_conditions.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } - if let Some(rule) = self.no_unused_expressions.as_ref() + if let Some(rule) = self.no_unresolved_imports.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); } - if let Some(rule) = self.no_useless_catch_binding.as_ref() + if let Some(rule) = self.no_unused_expressions.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } - if let Some(rule) = self.no_useless_undefined.as_ref() + if let Some(rule) = self.no_useless_catch_binding.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } - if let Some(rule) = self.no_vue_data_object_declaration.as_ref() + if let Some(rule) = self.no_useless_undefined.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } - if let Some(rule) = self.no_vue_duplicate_keys.as_ref() + if let Some(rule) = self.no_vue_data_object_declaration.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } - if let Some(rule) = self.no_vue_reserved_keys.as_ref() + if let Some(rule) = self.no_vue_duplicate_keys.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } - if let Some(rule) = self.no_vue_reserved_props.as_ref() + if let Some(rule) = self.no_vue_reserved_keys.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } - if let Some(rule) = self.no_vue_setup_props_reactivity_loss.as_ref() + if let Some(rule) = self.no_vue_reserved_props.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } - if let Some(rule) = self.no_vue_v_if_with_v_for.as_ref() + if let Some(rule) = self.no_vue_setup_props_reactivity_loss.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } - if let Some(rule) = self.use_array_sort_compare.as_ref() + if let Some(rule) = self.no_vue_v_if_with_v_for.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } - if let Some(rule) = self.use_await_thenable.as_ref() + if let Some(rule) = self.use_array_sort_compare.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } - if let Some(rule) = self.use_consistent_arrow_return.as_ref() + if let Some(rule) = self.use_await_thenable.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36])); } - if let Some(rule) = self.use_consistent_graphql_descriptions.as_ref() + if let Some(rule) = self.use_consistent_arrow_return.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); } - if let Some(rule) = self.use_deprecated_date.as_ref() + if let Some(rule) = self.use_consistent_graphql_descriptions.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])); } - if let Some(rule) = self.use_destructuring.as_ref() + if let Some(rule) = self.use_deprecated_date.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); } - if let Some(rule) = self.use_exhaustive_switch_cases.as_ref() + if let Some(rule) = self.use_destructuring.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40])); } - if let Some(rule) = self.use_explicit_type.as_ref() + if let Some(rule) = self.use_exhaustive_switch_cases.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41])); } - if let Some(rule) = self.use_find.as_ref() + if let Some(rule) = self.use_explicit_type.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42])); } - if let Some(rule) = self.use_max_params.as_ref() + if let Some(rule) = self.use_find.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43])); } - if let Some(rule) = self.use_qwik_method_usage.as_ref() + if let Some(rule) = self.use_max_params.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44])); } - if let Some(rule) = self.use_qwik_valid_lexical_scope.as_ref() + if let Some(rule) = self.use_qwik_method_usage.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45])); } - if let Some(rule) = self.use_regexp_exec.as_ref() + if let Some(rule) = self.use_qwik_valid_lexical_scope.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46])); } - if let Some(rule) = self.use_required_scripts.as_ref() + if let Some(rule) = self.use_regexp_exec.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47])); } - if let Some(rule) = self.use_sorted_classes.as_ref() + if let Some(rule) = self.use_required_scripts.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48])); } - if let Some(rule) = self.use_spread.as_ref() + if let Some(rule) = self.use_sorted_classes.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49])); } - if let Some(rule) = self.use_unique_graphql_operation_name.as_ref() + if let Some(rule) = self.use_spread.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50])); } - if let Some(rule) = self.use_vue_define_macros_order.as_ref() + if let Some(rule) = self.use_unique_graphql_operation_name.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51])); } - if let Some(rule) = self.use_vue_hyphenated_attributes.as_ref() + if let Some(rule) = self.use_vue_define_macros_order.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52])); } - if let Some(rule) = self.use_vue_multi_word_component_names.as_ref() + if let Some(rule) = self.use_vue_hyphenated_attributes.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53])); } - if let Some(rule) = self.use_vue_valid_v_bind.as_ref() + if let Some(rule) = self.use_vue_multi_word_component_names.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[54])); } - if let Some(rule) = self.use_vue_valid_v_else.as_ref() + if let Some(rule) = self.use_vue_valid_v_bind.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[55])); } - if let Some(rule) = self.use_vue_valid_v_else_if.as_ref() + if let Some(rule) = self.use_vue_valid_v_else.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[56])); } - if let Some(rule) = self.use_vue_valid_v_html.as_ref() + if let Some(rule) = self.use_vue_valid_v_else_if.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57])); } - if let Some(rule) = self.use_vue_valid_v_if.as_ref() + if let Some(rule) = self.use_vue_valid_v_html.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[58])); } - if let Some(rule) = self.use_vue_valid_v_on.as_ref() + if let Some(rule) = self.use_vue_valid_v_if.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[59])); } - if let Some(rule) = self.use_vue_valid_v_text.as_ref() + if let Some(rule) = self.use_vue_valid_v_on.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[60])); } + if let Some(rule) = self.use_vue_valid_v_text.as_ref() + && rule.is_disabled() + { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[61])); + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -5612,6 +5628,10 @@ impl RuleGroupExt for Nursery { rule_name: &str, ) -> Option<(RulePlainConfiguration, Option)> { match rule_name { + "noAmbiguousAnchorText" => self + .no_ambiguous_anchor_text + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noContinue" => self .no_continue .as_ref() @@ -5864,6 +5884,7 @@ impl From for Nursery { fn from(value: GroupPlainConfiguration) -> Self { Self { recommended: None, + no_ambiguous_anchor_text: Some(value.into()), no_continue: Some(value.into()), no_deprecated_imports: Some(value.into()), no_duplicate_dependencies: Some(value.into()), diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index cd8771bf19eb..debe7ea4a878 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -164,6 +164,7 @@ define_categories! { "lint/correctness/useValidForDirection": "https://biomejs.dev/linter/rules/use-valid-for-direction", "lint/correctness/useValidTypeof": "https://biomejs.dev/linter/rules/use-valid-typeof", "lint/correctness/useYield": "https://biomejs.dev/linter/rules/use-yield", + "lint/nursery/noAmbiguousAnchorText": "https://biomejs.dev/linter/rules/no-ambiguous-anchor-text", "lint/nursery/noColorInvalidHex": "https://biomejs.dev/linter/rules/no-color-invalid-hex", "lint/nursery/noContinue": "https://biomejs.dev/linter/rules/no-continue", "lint/nursery/noDeprecatedImports": "https://biomejs.dev/linter/rules/no-deprecated-imports", diff --git a/crates/biome_html_analyze/src/a11y.rs b/crates/biome_html_analyze/src/a11y.rs new file mode 100644 index 000000000000..6629417b88e5 --- /dev/null +++ b/crates/biome_html_analyze/src/a11y.rs @@ -0,0 +1,22 @@ +use biome_html_syntax::element_ext::AnyHtmlTagElement; + +/// Check the element is hidden from screen reader. +/// +/// Ref: +/// - https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-hidden +/// - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden +/// - https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.10.0/src/util/isHiddenFromScreenReader.js +pub(crate) fn is_hidden_from_screen_reader(element: &AnyHtmlTagElement) -> bool { + let is_aria_hidden = element.has_truthy_attribute("aria-hidden"); + if is_aria_hidden { + return true; + } + + match element.name_value_token().ok() { + Some(name) if name.text_trimmed() == "input" => element + .find_attribute_by_name("type") + .and_then(|attribute| attribute.initializer()?.value().ok()?.string_value()) + .is_some_and(|value| value.text() == "hidden"), + _ => false, + } +} diff --git a/crates/biome_html_analyze/src/lib.rs b/crates/biome_html_analyze/src/lib.rs index 375a264c2807..30850f0f47cd 100644 --- a/crates/biome_html_analyze/src/lib.rs +++ b/crates/biome_html_analyze/src/lib.rs @@ -1,5 +1,6 @@ #![deny(clippy::use_self)] +mod a11y; mod lint; pub mod options; mod registry; diff --git a/crates/biome_html_analyze/src/lint/nursery.rs b/crates/biome_html_analyze/src/lint/nursery.rs index ef733037648e..7a0564fc9cd2 100644 --- a/crates/biome_html_analyze/src/lint/nursery.rs +++ b/crates/biome_html_analyze/src/lint/nursery.rs @@ -3,6 +3,7 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use biome_analyze::declare_lint_group; +pub mod no_ambiguous_anchor_text; pub mod no_script_url; pub mod no_sync_scripts; pub mod no_vue_v_if_with_v_for; @@ -14,4 +15,4 @@ pub mod use_vue_valid_v_html; pub mod use_vue_valid_v_if; pub mod use_vue_valid_v_on; pub mod use_vue_valid_v_text; -declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_script_url :: NoScriptUrl , self :: no_sync_scripts :: NoSyncScripts , self :: no_vue_v_if_with_v_for :: NoVueVIfWithVFor , self :: use_vue_hyphenated_attributes :: UseVueHyphenatedAttributes , self :: use_vue_valid_v_bind :: UseVueValidVBind , self :: use_vue_valid_v_else :: UseVueValidVElse , self :: use_vue_valid_v_else_if :: UseVueValidVElseIf , self :: use_vue_valid_v_html :: UseVueValidVHtml , self :: use_vue_valid_v_if :: UseVueValidVIf , self :: use_vue_valid_v_on :: UseVueValidVOn , self :: use_vue_valid_v_text :: UseVueValidVText ,] } } +declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_script_url :: NoScriptUrl , self :: no_sync_scripts :: NoSyncScripts , self :: no_vue_v_if_with_v_for :: NoVueVIfWithVFor , self :: use_vue_hyphenated_attributes :: UseVueHyphenatedAttributes , self :: use_vue_valid_v_bind :: UseVueValidVBind , self :: use_vue_valid_v_else :: UseVueValidVElse , self :: use_vue_valid_v_else_if :: UseVueValidVElseIf , self :: use_vue_valid_v_html :: UseVueValidVHtml , self :: use_vue_valid_v_if :: UseVueValidVIf , self :: use_vue_valid_v_on :: UseVueValidVOn , self :: use_vue_valid_v_text :: UseVueValidVText ,] } } diff --git a/crates/biome_html_analyze/src/lint/nursery/no_ambiguous_anchor_text.rs b/crates/biome_html_analyze/src/lint/nursery/no_ambiguous_anchor_text.rs new file mode 100644 index 000000000000..8b79b3cdcd87 --- /dev/null +++ b/crates/biome_html_analyze/src/lint/nursery/no_ambiguous_anchor_text.rs @@ -0,0 +1,194 @@ +use biome_analyze::{ + Ast, QueryMatch, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, +}; +use biome_console::markup; +use biome_html_syntax::{ + AnyHtmlContent, AnyHtmlElement, HtmlElement, HtmlOpeningElement, + element_ext::AnyHtmlTagElement, inner_string_text, +}; +use biome_rowan::AstNode; +use biome_rule_options::no_ambiguous_anchor_text::NoAmbiguousAnchorTextOptions; +use biome_string_case::StrOnlyExtension; + +use crate::a11y::is_hidden_from_screen_reader; + +declare_lint_rule! { + /// Disallow ambiguous anchor descriptions. + /// + /// Enforces values are not exact matches for the phrases "click here", "here", "link", "a link", or "learn more". + /// Screen readers announce tags as links/interactive, but rely on values for context. + /// Ambiguous anchor descriptions do not provide sufficient context for users. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```html,expect_diagnostic + /// learn more + /// ``` + /// + /// ### Valid + /// + /// ```html + /// documentation + /// ``` + /// + /// ## Options + /// + /// ### `words` + /// + /// The words option allows users to modify the strings that can be checked for in the anchor text. Useful for specifying other words in other languages. + /// + /// Default `["click here", "here", "link", "a link", "learn more"]` + /// + /// ```json,options + /// { + /// "options": { + /// "words": ["click this"] + /// } + /// } + /// ``` + /// + /// #### Invalid + /// + /// ```html,expect_diagnostic,use_options + /// click this + /// ``` + /// + pub NoAmbiguousAnchorText { + version: "next", + name: "noAmbiguousAnchorText", + language: "html", + recommended: false, + sources: &[RuleSource::EslintJsxA11y("anchor-ambiguous-text").same()], + } +} + +impl Rule for NoAmbiguousAnchorText { + type Query = Ast; + type State = (); + type Signals = Option; + type Options = NoAmbiguousAnchorTextOptions; + + fn run(ctx: &RuleContext) -> Self::Signals { + let binding = ctx.query(); + let words = ctx.options().words(); + + let name = binding.name().ok()?; + let value_token = name.value_token().ok()?; + if value_token.text_trimmed() != "a" { + return None; + } + + let parent = HtmlElement::cast(binding.syntax().parent()?)?; + let text = get_accessible_child_text(&parent); + + if words.contains(&text) { + return Some(()); + } + + None + } + + fn diagnostic(ctx: &RuleContext, _state: &Self::State) -> Option { + let node = ctx.query(); + let parent = node.syntax().parent()?; + Some( + RuleDiagnostic::new( + rule_category!(), + parent.text_range(), + markup! { + "No ambiguous anchor descriptions allowed." + }, + ) + .note(markup! { + "Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users." + }), + ) + } +} + +fn get_aria_label(node: &AnyHtmlTagElement) -> Option { + let attribute = node.attributes().find_by_name("aria-label")?; + let initializer = attribute.initializer()?; + let value = initializer.value().ok()?; + let html_string = value.as_html_string()?; + let text = html_string.inner_string_text().ok()?; + + Some(text.to_string()) +} + +fn get_img_alt(node: &AnyHtmlTagElement) -> Option { + let name = node.name().ok()?; + let value_token = name.value_token().ok()?; + if value_token.text_trimmed() != "img" { + return None; + } + + let attribute = node.attributes().find_by_name("alt")?; + let initializer = attribute.initializer()?; + let value = initializer.value().ok()?; + let html_string = value.as_html_string()?; + let text = html_string.inner_string_text().ok()?; + + Some(text.to_string()) +} + +fn standardize_space_and_case(input: &str) -> String { + input + .chars() + .filter(|c| !matches!(c, ',' | '.' | '?' | '¿' | '!' | '‽' | '¡' | ';' | ':')) + .collect::() + .to_lowercase_cow() + .split_whitespace() + .collect::>() + .join(" ") +} + +fn get_accessible_text(node: &AnyHtmlTagElement) -> Option { + if is_hidden_from_screen_reader(node) { + return Some(String::new()); + } + + if let Some(aria_label) = get_aria_label(node) { + return Some(standardize_space_and_case(&aria_label)); + } + + if let Some(alt) = get_img_alt(node) { + return Some(standardize_space_and_case(&alt)); + } + + None +} + +fn get_accessible_child_text(node: &HtmlElement) -> String { + if let Ok(opening) = node.opening_element() { + let any_jsx_element: AnyHtmlTagElement = opening.clone().into(); + if let Some(accessible_text) = get_accessible_text(&any_jsx_element) { + return accessible_text; + } + }; + + let raw_child_text = node + .children() + .into_iter() + .map(|child| match child { + AnyHtmlElement::AnyHtmlContent(AnyHtmlContent::HtmlContent(content)) => { + if let Ok(value_token) = content.value_token() { + inner_string_text(&value_token).to_string() + } else { + String::new() + } + } + AnyHtmlElement::HtmlElement(element) => get_accessible_child_text(&element), + AnyHtmlElement::HtmlSelfClosingElement(element) => { + let any_jsx_element: AnyHtmlTagElement = element.clone().into(); + get_accessible_text(&any_jsx_element).unwrap_or_default() + } + _ => String::new(), + }) + .collect::>() + .join(" "); + + standardize_space_and_case(&raw_child_text) +} diff --git a/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/invalid.html b/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/invalid.html new file mode 100644 index 000000000000..47b8e487b6ae --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/invalid.html @@ -0,0 +1,49 @@ +/* should generate diagnostics */ + +here + +HERE + +click here + +learn more + +learn more + +learn more. + +learn more? + +learn more, + +learn more! + +learn more; + +learn more: + +link + +a link + +something + + a link + +a link + +a link + +click here + + click here + +more textlearn more + +learn more + +click here + +click here + +click here diff --git a/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/invalid.html.snap b/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/invalid.html.snap new file mode 100644 index 000000000000..5f80988c63a2 --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/invalid.html.snap @@ -0,0 +1,465 @@ +--- +source: crates/biome_html_analyze/tests/spec_tests.rs +expression: invalid.html +--- +# Input +```html +/* should generate diagnostics */ + +here + +HERE + +click here + +learn more + +learn more + +learn more. + +learn more? + +learn more, + +learn more! + +learn more; + +learn more: + +link + +a link + +something + + a link + +a link + +a link + +click here + + click here + +more textlearn more + +learn more + +click here + +click here + +click here + +``` + +# Diagnostics +``` +invalid.html:3:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 1 │ /* should generate diagnostics */ + 2 │ + > 3 │ here + │ ^^^^^^^^^^^ + 4 │ + 5 │ HERE + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:5:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 3 │ here + 4 │ + > 5 │ HERE + │ ^^^^^^^^^^^ + 6 │ + 7 │ click here + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:7:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 5 │ HERE + 6 │ + > 7 │ click here + │ ^^^^^^^^^^^^^^^^^ + 8 │ + 9 │ learn more + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:9:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 7 │ click here + 8 │ + > 9 │ learn more + │ ^^^^^^^^^^^^^^^^^ + 10 │ + 11 │ learn more + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:11:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 9 │ learn more + 10 │ + > 11 │ learn more + │ ^^^^^^^^^^^^^^^^^^^^^^ + 12 │ + 13 │ learn more. + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:13:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 11 │ learn more + 12 │ + > 13 │ learn more. + │ ^^^^^^^^^^^^^^^^^^ + 14 │ + 15 │ learn more? + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:15:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 13 │ learn more. + 14 │ + > 15 │ learn more? + │ ^^^^^^^^^^^^^^^^^^ + 16 │ + 17 │ learn more, + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:17:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 15 │ learn more? + 16 │ + > 17 │ learn more, + │ ^^^^^^^^^^^^^^^^^^ + 18 │ + 19 │ learn more! + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:19:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 17 │ learn more, + 18 │ + > 19 │ learn more! + │ ^^^^^^^^^^^^^^^^^^ + 20 │ + 21 │ learn more; + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:21:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 19 │ learn more! + 20 │ + > 21 │ learn more; + │ ^^^^^^^^^^^^^^^^^^ + 22 │ + 23 │ learn more: + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:23:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 21 │ learn more; + 22 │ + > 23 │ learn more: + │ ^^^^^^^^^^^^^^^^^^ + 24 │ + 25 │ link + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:25:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 23 │ learn more: + 24 │ + > 25 │ link + │ ^^^^^^^^^^^ + 26 │ + 27 │ a link + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:27:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 25 │ link + 26 │ + > 27 │ a link + │ ^^^^^^^^^^^^^ + 28 │ + 29 │ something + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:29:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 27 │ a link + 28 │ + > 29 │ something + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 30 │ + 31 │ a link + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:31:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 29 │ something + 30 │ + > 31 │ a link + │ ^^^^^^^^^^^^^^^ + 32 │ + 33 │ a link + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:33:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 31 │ a link + 32 │ + > 33 │ a link + │ ^^^^^^^^^^^^^^^^^^^^ + 34 │ + 35 │ a link + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:35:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 33 │ a link + 34 │ + > 35 │ a link + │ ^^^^^^^^^^^^^^^^^^^^ + 36 │ + 37 │ click here + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:37:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 35 │ a link + 36 │ + > 37 │ click here + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 38 │ + 39 │ click here + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:39:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 37 │ click here + 38 │ + > 39 │ click here + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 40 │ + 41 │ more textlearn more + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:41:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 39 │ click here + 40 │ + > 41 │ more textlearn more + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 42 │ + 43 │ learn more + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:43:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 41 │ more textlearn more + 42 │ + > 43 │ learn more + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 44 │ + 45 │ click here + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:45:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 43 │ learn more + 44 │ + > 45 │ click here + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 46 │ + 47 │ click here + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:47:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 45 │ click here + 46 │ + > 47 │ click here + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 48 │ + 49 │ click here + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.html:49:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 47 │ click here + 48 │ + > 49 │ click here + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 50 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` diff --git a/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/valid.html b/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/valid.html new file mode 100644 index 000000000000..196755756e0c --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/valid.html @@ -0,0 +1,8 @@ +/* should not generate diagnostics */ +documentation + +click here + +click here + +documentation diff --git a/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/valid.html.snap b/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/valid.html.snap new file mode 100644 index 000000000000..e508049e9d92 --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/valid.html.snap @@ -0,0 +1,16 @@ +--- +source: crates/biome_html_analyze/tests/spec_tests.rs +expression: valid.html +--- +# Input +```html +/* should not generate diagnostics */ +documentation + +click here + +click here + +documentation + +``` diff --git a/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.html b/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.html new file mode 100644 index 000000000000..7df1e6c9c78e --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.html @@ -0,0 +1,2 @@ +/* should generate diagnostics */ +a disallowed word diff --git a/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.html.snap b/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.html.snap new file mode 100644 index 000000000000..0421c74e6d0f --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.html.snap @@ -0,0 +1,26 @@ +--- +source: crates/biome_html_analyze/tests/spec_tests.rs +expression: invalid.html +--- +# Input +```html +/* should generate diagnostics */ +a disallowed word + +``` + +# Diagnostics +``` +invalid.html:2:1 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 1 │ /* should generate diagnostics */ + > 2 │ a disallowed word + │ ^^^^^^^^^^^^^^^^^^^^^^^^ + 3 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` diff --git a/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.options.json b/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.options.json new file mode 100644 index 000000000000..1a5640867583 --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.options.json @@ -0,0 +1,17 @@ +{ + "$schema": "../../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "rules": { + "nursery": { + "noAmbiguousAnchorText": { + "level": "on", + "options": { + "words": [ + "a disallowed word" + ] + } + } + } + } + } +} diff --git a/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.html b/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.html new file mode 100644 index 000000000000..bab7b1f01d6e --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.html @@ -0,0 +1,2 @@ +/* should not generate diagnostics */ +click here diff --git a/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.html.snap b/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.html.snap new file mode 100644 index 000000000000..6d37d63ce5bf --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.html.snap @@ -0,0 +1,10 @@ +--- +source: crates/biome_html_analyze/tests/spec_tests.rs +expression: valid.html +--- +# Input +```html +/* should not generate diagnostics */ +click here + +``` diff --git a/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.options.json b/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.options.json new file mode 100644 index 000000000000..7b821b9baefc --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.options.json @@ -0,0 +1,17 @@ +{ + "$schema": "../../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "rules": { + "nursery": { + "noAmbiguousAnchorText": { + "level": "on", + "options": { + "words": [ + "disabling the defaults" + ] + } + } + } + } + } +} diff --git a/crates/biome_html_syntax/src/element_ext.rs b/crates/biome_html_syntax/src/element_ext.rs index ed51618c4d13..50a35513284d 100644 --- a/crates/biome_html_syntax/src/element_ext.rs +++ b/crates/biome_html_syntax/src/element_ext.rs @@ -1,6 +1,7 @@ use crate::{ - AnyHtmlElement, AstroEmbeddedContent, HtmlAttribute, HtmlElement, HtmlEmbeddedContent, - HtmlSelfClosingElement, HtmlSyntaxToken, HtmlTagName, ScriptType, inner_string_text, + AnyHtmlElement, AstroEmbeddedContent, HtmlAttribute, HtmlAttributeList, HtmlElement, + HtmlEmbeddedContent, HtmlOpeningElement, HtmlSelfClosingElement, HtmlSyntaxToken, HtmlTagName, + ScriptType, inner_string_text, }; use biome_rowan::{AstNodeList, SyntaxResult, TokenText, declare_node_union}; @@ -88,25 +89,29 @@ impl HtmlSelfClosingElement { } } +impl HtmlOpeningElement { + pub fn find_attribute_by_name(&self, name_to_lookup: &str) -> Option { + self.attributes().iter().find_map(|attr| { + let attribute = attr.as_html_attribute()?; + let name = attribute.name().ok()?; + let name_token = name.value_token().ok()?; + if name_token + .text_trimmed() + .eq_ignore_ascii_case(name_to_lookup) + { + Some(attribute.clone()) + } else { + None + } + }) + } +} + impl HtmlElement { pub fn find_attribute_by_name(&self, name_to_lookup: &str) -> Option { self.opening_element() .ok()? - .attributes() - .iter() - .find_map(|attr| { - let attribute = attr.as_html_attribute()?; - let name = attribute.name().ok()?; - let name_token = name.value_token().ok()?; - if name_token - .text_trimmed() - .eq_ignore_ascii_case(name_to_lookup) - { - Some(attribute.clone()) - } else { - None - } - }) + .find_attribute_by_name(name_to_lookup) } pub fn is_javascript_tag(&self) -> bool { @@ -225,6 +230,48 @@ impl HtmlTagName { } } +declare_node_union! { + pub AnyHtmlTagElement = HtmlOpeningElement | HtmlSelfClosingElement +} + +impl AnyHtmlTagElement { + pub fn name(&self) -> SyntaxResult { + match self { + Self::HtmlOpeningElement(element) => element.name(), + Self::HtmlSelfClosingElement(element) => element.name(), + } + } + + pub fn attributes(&self) -> HtmlAttributeList { + match self { + Self::HtmlOpeningElement(element) => element.attributes(), + Self::HtmlSelfClosingElement(element) => element.attributes(), + } + } + + pub fn name_value_token(&self) -> SyntaxResult { + self.name()?.value_token() + } + + pub fn find_attribute_by_name(&self, name_to_lookup: &str) -> Option { + match self { + Self::HtmlOpeningElement(element) => element.find_attribute_by_name(name_to_lookup), + Self::HtmlSelfClosingElement(element) => element.find_attribute_by_name(name_to_lookup), + } + } + + pub fn has_truthy_attribute(&self, name_to_lookup: &str) -> bool { + self.find_attribute_by_name(name_to_lookup) + .is_some_and(|attribute| { + attribute + .initializer() + .and_then(|init| init.value().ok()) + .and_then(|value| value.string_value()) + .is_none_or(|value| value != "false") + }) + } +} + #[cfg(test)] mod tests { use biome_html_factory::syntax::HtmlElement; diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index 7ec664d3d9cd..11a0a0f0af70 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -3,6 +3,7 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use biome_analyze::declare_lint_group; +pub mod no_ambiguous_anchor_text; pub mod no_continue; pub mod no_deprecated_imports; pub mod no_duplicated_spread_props; @@ -50,4 +51,4 @@ pub mod use_sorted_classes; pub mod use_spread; pub mod use_vue_define_macros_order; pub mod use_vue_multi_word_component_names; -declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_multi_str :: NoMultiStr , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_proto :: NoProto , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_script_url :: NoScriptUrl , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: no_vue_setup_props_reactivity_loss :: NoVueSetupPropsReactivityLoss , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_await_thenable :: UseAwaitThenable , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_destructuring :: UseDestructuring , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_regexp_exec :: UseRegexpExec , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } } +declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_multi_str :: NoMultiStr , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_proto :: NoProto , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_script_url :: NoScriptUrl , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: no_vue_setup_props_reactivity_loss :: NoVueSetupPropsReactivityLoss , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_await_thenable :: UseAwaitThenable , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_destructuring :: UseDestructuring , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_regexp_exec :: UseRegexpExec , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } } diff --git a/crates/biome_js_analyze/src/lint/nursery/no_ambiguous_anchor_text.rs b/crates/biome_js_analyze/src/lint/nursery/no_ambiguous_anchor_text.rs new file mode 100644 index 000000000000..37cb3b7b9ac4 --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/no_ambiguous_anchor_text.rs @@ -0,0 +1,194 @@ +use biome_analyze::{ + Ast, QueryMatch, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, +}; +use biome_console::markup; +use biome_js_syntax::{ + AnyJsxChild, JsxElement, JsxOpeningElement, inner_string_text, jsx_ext::AnyJsxElement, +}; +use biome_rowan::AstNode; +use biome_rule_options::no_ambiguous_anchor_text::NoAmbiguousAnchorTextOptions; +use biome_string_case::StrOnlyExtension; + +use crate::a11y::is_hidden_from_screen_reader; + +declare_lint_rule! { + /// Disallow ambiguous anchor descriptions. + /// + /// Enforces values are not exact matches for the phrases "click here", "here", "link", "a link", or "learn more". + /// Screen readers announce tags as links/interactive, but rely on values for context. + /// Ambiguous anchor descriptions do not provide sufficient context for users. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```jsx,expect_diagnostic + /// const Invalid = () => learn more; + /// ``` + /// + /// ### Valid + /// + /// ```jsx + /// const Valid = () => documentation; + /// ``` + /// + /// ## Options + /// + /// ### `words` + /// + /// The words option allows users to modify the strings that can be checked for in the anchor text. Useful for specifying other words in other languages. + /// + /// Default `["click here", "here", "link", "a link", "learn more"]` + /// + /// ```json,options + /// { + /// "options": { + /// "words": ["click this"] + /// } + /// } + /// ``` + /// + /// #### Invalid + /// + /// ```jsx,expect_diagnostic,use_options + /// const Invalid = () => click this; + /// ``` + /// + pub NoAmbiguousAnchorText { + version: "next", + name: "noAmbiguousAnchorText", + language: "js", + recommended: false, + sources: &[RuleSource::EslintJsxA11y("anchor-ambiguous-text").same()], + } +} + +impl Rule for NoAmbiguousAnchorText { + type Query = Ast; + type State = (); + type Signals = Option; + type Options = NoAmbiguousAnchorTextOptions; + + fn run(ctx: &RuleContext) -> Self::Signals { + let binding = ctx.query(); + let words = ctx.options().words(); + + let name = binding.name().ok()?; + let jsx_name = name.as_jsx_name()?; + let value_token = jsx_name.value_token().ok()?; + if value_token.text_trimmed() != "a" { + return None; + } + + let parent = JsxElement::cast(binding.syntax().parent()?)?; + let text = get_accessible_child_text(&parent); + + if words.contains(&text) { + return Some(()); + } + + None + } + + fn diagnostic(ctx: &RuleContext, _state: &Self::State) -> Option { + let node = ctx.query(); + let parent = node.syntax().parent()?; + Some( + RuleDiagnostic::new( + rule_category!(), + parent.text_range(), + markup! { + "No ambiguous anchor descriptions allowed." + }, + ) + .note(markup! { + "Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users." + }), + ) + } +} + +fn get_aria_label(node: &AnyJsxElement) -> Option { + let attribute = node.attributes().find_by_name("aria-label")?; + let initializer = attribute.initializer()?; + let value = initializer.value().ok()?; + let text = value.as_jsx_string()?.inner_string_text().ok()?; + + Some(text.to_string()) +} + +fn get_img_alt(node: &AnyJsxElement) -> Option { + let name = node.name().ok()?; + let jsx_name = name.as_jsx_name()?; + let value_token = jsx_name.value_token().ok()?; + if value_token.text_trimmed() != "img" { + return None; + } + + let attribute = node.attributes().find_by_name("alt")?; + let initializer = attribute.initializer()?; + let value = initializer.value().ok()?; + let jsx_string = value.as_jsx_string()?; + let text = jsx_string.inner_string_text().ok()?; + + Some(text.to_string()) +} + +fn standardize_space_and_case(input: &str) -> String { + input + .chars() + .filter(|c| !matches!(c, ',' | '.' | '?' | '¿' | '!' | '‽' | '¡' | ';' | ':')) + .collect::() + .to_lowercase_cow() + .split_whitespace() + .collect::>() + .join(" ") +} + +fn get_accessible_text(node: &AnyJsxElement) -> Option { + if is_hidden_from_screen_reader(node) { + return Some(String::new()); + } + + if let Some(aria_label) = get_aria_label(node) { + return Some(standardize_space_and_case(&aria_label)); + } + + if let Some(alt) = get_img_alt(node) { + return Some(standardize_space_and_case(&alt)); + } + + None +} + +fn get_accessible_child_text(node: &JsxElement) -> String { + if let Ok(opening) = node.opening_element() { + let any_jsx_element: AnyJsxElement = opening.clone().into(); + if let Some(accessible_text) = get_accessible_text(&any_jsx_element) { + return accessible_text; + } + }; + + let raw_child_text = node + .children() + .into_iter() + .map(|child| match child { + AnyJsxChild::JsxText(element) => { + if let Ok(value_token) = element.value_token() { + inner_string_text(&value_token).to_string() + } else { + String::new() + } + } + AnyJsxChild::JsxElement(element) => get_accessible_child_text(&element), + AnyJsxChild::JsxSelfClosingElement(element) => { + let any_jsx_element: AnyJsxElement = element.clone().into(); + get_accessible_text(&any_jsx_element).unwrap_or_default() + } + _ => String::new(), + }) + .collect::>() + .join(" "); + + standardize_space_and_case(&raw_child_text) +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/invalid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/invalid.jsx new file mode 100644 index 000000000000..8f4457e2bca5 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/invalid.jsx @@ -0,0 +1,100 @@ +/* should generate diagnostics */ +const Invalid1 = () => { + return here; +} + +const Invalid2 = () => { + return HERE; +} + +const Invalid3 = () => { + return click here; +} + +const Invalid4 = () => { + return learn more; +} + +const Invalid5 = () => { + return learn more; +} + +const Invalid6 = () => { + return learn more.; +} + +const Invalid7 = () => { + return learn more?; +} + +const Invalid8 = () => { + return learn more,; +} + +const Invalid9 = () => { + return learn more!; +} + +const Invalid10 = () => { + return learn more;; +} + +const Invalid11 = () => { + return learn more:; +} + +const Invalid12 = () => { + return link; +} + +const Invalid13 = () => { + return a link; +} + +const Invalid14 = () => { + return something; +} + +const Invalid15 = () => { + return a link ; +} + +const Invalid16 = () => { + return a link; +} + +const Invalid17 = () => { + return a link; +} + +const Invalid18 = () => { + return click here; +} + +const Invalid19 = () => { + return click here; +} + +const Invalid20 = () => { + return more textlearn more; +} + +const Invalid21 = () => { + return learn more; +} + +const Invalid22 = () => { + return click here; +} + +const Invalid23 = () => { + return click here; +} + +const Invalid24 = () => { + return click here; +} + +const Invalid25 = () => { + return click here; +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/invalid.jsx.snap new file mode 100644 index 000000000000..3c9c16cd1f88 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/invalid.jsx.snap @@ -0,0 +1,510 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.jsx +--- +# Input +```jsx +/* should generate diagnostics */ +const Invalid1 = () => { + return here; +} + +const Invalid2 = () => { + return HERE; +} + +const Invalid3 = () => { + return click here; +} + +const Invalid4 = () => { + return learn more; +} + +const Invalid5 = () => { + return learn more; +} + +const Invalid6 = () => { + return learn more.; +} + +const Invalid7 = () => { + return learn more?; +} + +const Invalid8 = () => { + return learn more,; +} + +const Invalid9 = () => { + return learn more!; +} + +const Invalid10 = () => { + return learn more;; +} + +const Invalid11 = () => { + return learn more:; +} + +const Invalid12 = () => { + return link; +} + +const Invalid13 = () => { + return a link; +} + +const Invalid14 = () => { + return something; +} + +const Invalid15 = () => { + return a link ; +} + +const Invalid16 = () => { + return a link; +} + +const Invalid17 = () => { + return a link; +} + +const Invalid18 = () => { + return click here; +} + +const Invalid19 = () => { + return click here; +} + +const Invalid20 = () => { + return more textlearn more; +} + +const Invalid21 = () => { + return learn more; +} + +const Invalid22 = () => { + return click here; +} + +const Invalid23 = () => { + return click here; +} + +const Invalid24 = () => { + return click here; +} + +const Invalid25 = () => { + return click here; +} + +``` + +# Diagnostics +``` +invalid.jsx:3:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 1 │ /* should generate diagnostics */ + 2 │ const Invalid1 = () => { + > 3 │ return here; + │ ^^^^^^^^^^^ + 4 │ } + 5 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:7:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 6 │ const Invalid2 = () => { + > 7 │ return HERE; + │ ^^^^^^^^^^^ + 8 │ } + 9 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:11:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 10 │ const Invalid3 = () => { + > 11 │ return click here; + │ ^^^^^^^^^^^^^^^^^ + 12 │ } + 13 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:15:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 14 │ const Invalid4 = () => { + > 15 │ return learn more; + │ ^^^^^^^^^^^^^^^^^ + 16 │ } + 17 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:19:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 18 │ const Invalid5 = () => { + > 19 │ return learn more; + │ ^^^^^^^^^^^^^^^^^^^^^^ + 20 │ } + 21 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:23:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 22 │ const Invalid6 = () => { + > 23 │ return learn more.; + │ ^^^^^^^^^^^^^^^^^^ + 24 │ } + 25 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:27:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 26 │ const Invalid7 = () => { + > 27 │ return learn more?; + │ ^^^^^^^^^^^^^^^^^^ + 28 │ } + 29 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:31:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 30 │ const Invalid8 = () => { + > 31 │ return learn more,; + │ ^^^^^^^^^^^^^^^^^^ + 32 │ } + 33 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:35:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 34 │ const Invalid9 = () => { + > 35 │ return learn more!; + │ ^^^^^^^^^^^^^^^^^^ + 36 │ } + 37 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:39:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 38 │ const Invalid10 = () => { + > 39 │ return learn more;; + │ ^^^^^^^^^^^^^^^^^^ + 40 │ } + 41 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:43:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 42 │ const Invalid11 = () => { + > 43 │ return learn more:; + │ ^^^^^^^^^^^^^^^^^^ + 44 │ } + 45 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:47:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 46 │ const Invalid12 = () => { + > 47 │ return link; + │ ^^^^^^^^^^^ + 48 │ } + 49 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:51:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 50 │ const Invalid13 = () => { + > 51 │ return a link; + │ ^^^^^^^^^^^^^ + 52 │ } + 53 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:55:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 54 │ const Invalid14 = () => { + > 55 │ return something; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 56 │ } + 57 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:59:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 58 │ const Invalid15 = () => { + > 59 │ return a link ; + │ ^^^^^^^^^^^^^^^ + 60 │ } + 61 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:63:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 62 │ const Invalid16 = () => { + > 63 │ return a link; + │ ^^^^^^^^^^^^^^^^^^^^ + 64 │ } + 65 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:67:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 66 │ const Invalid17 = () => { + > 67 │ return a link; + │ ^^^^^^^^^^^^^^^^^^^^ + 68 │ } + 69 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:71:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 70 │ const Invalid18 = () => { + > 71 │ return click here; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 72 │ } + 73 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:75:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 74 │ const Invalid19 = () => { + > 75 │ return click here; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 76 │ } + 77 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:79:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 78 │ const Invalid20 = () => { + > 79 │ return more textlearn more; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 80 │ } + 81 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:83:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 82 │ const Invalid21 = () => { + > 83 │ return learn more; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 84 │ } + 85 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:87:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 86 │ const Invalid22 = () => { + > 87 │ return click here; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 88 │ } + 89 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:91:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 90 │ const Invalid23 = () => { + > 91 │ return click here; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 92 │ } + 93 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:95:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 94 │ const Invalid24 = () => { + > 95 │ return click here; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 96 │ } + 97 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` + +``` +invalid.jsx:99:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 98 │ const Invalid25 = () => { + > 99 │ return click here; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 100 │ } + 101 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/valid.jsx new file mode 100644 index 000000000000..ff7f52696513 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/valid.jsx @@ -0,0 +1,20 @@ +/* should not generate diagnostics */ +const Valid1 = () => { + return documentation; +} + +const Valid2 = () => { + return ${here}; +} + +const Valid3 = () => { + return click here; +} + +const Valid4 = () => { + return click here; +} + +const Valid5 = () => { + return documentation; +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/valid.jsx.snap new file mode 100644 index 000000000000..5269326bd007 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/valid.jsx.snap @@ -0,0 +1,28 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.jsx +--- +# Input +```jsx +/* should not generate diagnostics */ +const Valid1 = () => { + return documentation; +} + +const Valid2 = () => { + return ${here}; +} + +const Valid3 = () => { + return click here; +} + +const Valid4 = () => { + return click here; +} + +const Valid5 = () => { + return documentation; +} + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.jsx new file mode 100644 index 000000000000..8c37cb53b461 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.jsx @@ -0,0 +1,4 @@ +/* should generate diagnostics */ +const Invalid = () => { + return a disallowed word; +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.jsx.snap new file mode 100644 index 000000000000..367a62483145 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.jsx.snap @@ -0,0 +1,30 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.jsx +--- +# Input +```jsx +/* should generate diagnostics */ +const Invalid = () => { + return a disallowed word; +} + +``` + +# Diagnostics +``` +invalid.jsx:3:9 lint/nursery/noAmbiguousAnchorText ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i No ambiguous anchor descriptions allowed. + + 1 │ /* should generate diagnostics */ + 2 │ const Invalid = () => { + > 3 │ return a disallowed word; + │ ^^^^^^^^^^^^^^^^^^^^^^^^ + 4 │ } + 5 │ + + i Ambiguous anchor descriptions do not provide sufficient context for screen reader users. Provide more context to these users. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.options.json b/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.options.json new file mode 100644 index 000000000000..1a5640867583 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/invalid.options.json @@ -0,0 +1,17 @@ +{ + "$schema": "../../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "rules": { + "nursery": { + "noAmbiguousAnchorText": { + "level": "on", + "options": { + "words": [ + "a disallowed word" + ] + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.jsx new file mode 100644 index 000000000000..6eecccca2224 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.jsx @@ -0,0 +1,4 @@ +/* should not generate diagnostics */ +const Valid = () => { + return click here; +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.jsx.snap new file mode 100644 index 000000000000..53ead33df97c --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.jsx.snap @@ -0,0 +1,12 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.jsx +--- +# Input +```jsx +/* should not generate diagnostics */ +const Valid = () => { + return click here; +} + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.options.json b/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.options.json new file mode 100644 index 000000000000..7b821b9baefc --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noAmbiguousAnchorText/words/valid.options.json @@ -0,0 +1,17 @@ +{ + "$schema": "../../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "rules": { + "nursery": { + "noAmbiguousAnchorText": { + "level": "on", + "options": { + "words": [ + "disabling the defaults" + ] + } + } + } + } + } +} diff --git a/crates/biome_rule_options/src/lib.rs b/crates/biome_rule_options/src/lib.rs index 721f83798e2b..49c1d7211f55 100644 --- a/crates/biome_rule_options/src/lib.rs +++ b/crates/biome_rule_options/src/lib.rs @@ -6,6 +6,7 @@ pub mod no_access_key; pub mod no_accumulating_spread; pub mod no_adjacent_spaces_in_regex; pub mod no_alert; +pub mod no_ambiguous_anchor_text; pub mod no_approximative_numeric_constant; pub mod no_arguments; pub mod no_aria_hidden_on_focusable; diff --git a/crates/biome_rule_options/src/no_ambiguous_anchor_text.rs b/crates/biome_rule_options/src/no_ambiguous_anchor_text.rs new file mode 100644 index 000000000000..c2dd9a51aacd --- /dev/null +++ b/crates/biome_rule_options/src/no_ambiguous_anchor_text.rs @@ -0,0 +1,25 @@ +use biome_deserialize_macros::{Deserializable, Merge}; +use serde::{Deserialize, Serialize}; +#[derive(Default, Clone, Debug, Deserialize, Deserializable, Merge, Eq, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields, default)] +pub struct NoAmbiguousAnchorTextOptions { + /// It allows users to modify the strings that can be checked for in the anchor text. Useful for specifying other words in other languages + pub words: Option>, +} + +impl NoAmbiguousAnchorTextOptions { + pub const DEFAULT_AMBIGUOUS_WORDS: [&str; 5] = + ["click here", "here", "link", "a link", "learn more"]; + + /// Returns [`Self::words`] if it is set. + /// Otherwise, returns [`Self::DEFAULT_AMBIGUOUS_WORDS`]. + pub fn words(&self) -> Vec { + self.words.clone().unwrap_or_else(|| { + Self::DEFAULT_AMBIGUOUS_WORDS + .iter() + .map(|s| (*s).to_string()) + .collect() + }) + } +} diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index b70818b696f9..f0ac3be1982c 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1856,6 +1856,11 @@ See * A list of rules that belong to this group */ export interface Nursery { + /** + * Disallow ambiguous anchor descriptions. +See + */ + noAmbiguousAnchorText?: NoAmbiguousAnchorTextConfiguration; /** * Disallow continue statements. See @@ -3589,6 +3594,9 @@ export type UseValidTypeofConfiguration = export type UseYieldConfiguration = | RulePlainConfiguration | RuleWithUseYieldOptions; +export type NoAmbiguousAnchorTextConfiguration = + | RulePlainConfiguration + | RuleWithNoAmbiguousAnchorTextOptions; export type NoContinueConfiguration = | RulePlainConfiguration | RuleWithNoContinueOptions; @@ -4999,6 +5007,10 @@ export interface RuleWithUseYieldOptions { level: RulePlainConfiguration; options?: UseYieldOptions; } +export interface RuleWithNoAmbiguousAnchorTextOptions { + level: RulePlainConfiguration; + options?: NoAmbiguousAnchorTextOptions; +} export interface RuleWithNoContinueOptions { level: RulePlainConfiguration; options?: NoContinueOptions; @@ -6352,6 +6364,12 @@ to a DOM element id. export type UseValidForDirectionOptions = {}; export type UseValidTypeofOptions = {}; export type UseYieldOptions = {}; +export interface NoAmbiguousAnchorTextOptions { + /** + * It allows users to modify the strings that can be checked for in the anchor text. Useful for specifying other words in other languages + */ + words?: string[]; +} export type NoContinueOptions = {}; export type NoDeprecatedImportsOptions = {}; export type NoDuplicateDependenciesOptions = {}; @@ -7227,6 +7245,7 @@ export type Category = | "lint/correctness/useValidForDirection" | "lint/correctness/useValidTypeof" | "lint/correctness/useYield" + | "lint/nursery/noAmbiguousAnchorText" | "lint/nursery/noColorInvalidHex" | "lint/nursery/noContinue" | "lint/nursery/noDeprecatedImports" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 15ce072e8246..0fce6b608756 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -2470,6 +2470,24 @@ ] }, "NoAlertOptions": { "type": "object", "additionalProperties": false }, + "NoAmbiguousAnchorTextConfiguration": { + "oneOf": [ + { "$ref": "#/$defs/RulePlainConfiguration" }, + { "$ref": "#/$defs/RuleWithNoAmbiguousAnchorTextOptions" } + ] + }, + "NoAmbiguousAnchorTextOptions": { + "type": "object", + "properties": { + "words": { + "description": "It allows users to modify the strings that can be checked for in the anchor text. Useful for specifying other words in other languages", + "type": ["array", "null"], + "default": null, + "items": { "type": "string" } + } + }, + "additionalProperties": false + }, "NoApproximativeNumericConstantConfiguration": { "oneOf": [ { "$ref": "#/$defs/RulePlainConfiguration" }, @@ -5102,6 +5120,13 @@ "description": "A list of rules that belong to this group", "type": "object", "properties": { + "noAmbiguousAnchorText": { + "description": "Disallow ambiguous anchor descriptions.\nSee ", + "anyOf": [ + { "$ref": "#/$defs/NoAmbiguousAnchorTextConfiguration" }, + { "type": "null" } + ] + }, "noContinue": { "description": "Disallow continue statements.\nSee ", "anyOf": [ @@ -6082,6 +6107,15 @@ "additionalProperties": false, "required": ["level"] }, + "RuleWithNoAmbiguousAnchorTextOptions": { + "type": "object", + "properties": { + "level": { "$ref": "#/$defs/RulePlainConfiguration" }, + "options": { "$ref": "#/$defs/NoAmbiguousAnchorTextOptions" } + }, + "additionalProperties": false, + "required": ["level"] + }, "RuleWithNoApproximativeNumericConstantOptions": { "type": "object", "properties": {