diff --git a/.changeset/add-no-script-url-rule.md b/.changeset/add-no-script-url-rule.md new file mode 100644 index 000000000000..6a0ccc52c112 --- /dev/null +++ b/.changeset/add-no-script-url-rule.md @@ -0,0 +1,10 @@ +--- +"@biomejs/biome": patch +--- +Added the nursery rule [`noScriptUrl`](https://biomejs.dev/linter/rules/no-script-url/). + +This rule disallows the use of `javascript:` URLs, which are considered a form of `eval` and can pose security risks such as XSS vulnerabilities. + +```jsx +Click me +``` 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 fd4d22d100c6..716706ff1210 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 @@ -9,6 +9,18 @@ pub(crate) fn migrate_eslint_any_rule( results: &mut eslint_to_biome::MigrationResults, ) -> bool { match eslint_name { + "@eslint-react/dom-no-script-url" => { + 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_script_url + .get_or_insert(Default::default()); + rule.set_level(rule.level().max(rule_severity.into())); + } "@eslint-react/no-forward-ref" => { if !options.include_nursery { results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery); @@ -1897,6 +1909,18 @@ pub(crate) fn migrate_eslint_any_rule( .get_or_insert(Default::default()); rule.set_level(rule.level().max(rule_severity.into())); } + "no-script-url" => { + 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_script_url + .get_or_insert(Default::default()); + rule.set_level(rule.level().max(rule_severity.into())); + } "no-secrets/no-secrets" => { if !options.include_inspired { results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Inspired); @@ -2373,6 +2397,18 @@ pub(crate) fn migrate_eslint_any_rule( .get_or_insert(Default::default()); rule.set_level(rule.level().max(rule_severity.into())); } + "qwik/jsx-no-script-url" => { + 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_script_url + .get_or_insert(Default::default()); + rule.set_level(rule.level().max(rule_severity.into())); + } "qwik/no-react-props" => { let group = rules.suspicious.get_or_insert_with(Default::default); let rule = group @@ -2585,6 +2621,18 @@ pub(crate) fn migrate_eslint_any_rule( .get_or_insert(Default::default()); rule.set_level(rule.level().max(rule_severity.into())); } + "react/jsx-no-script-url" => { + 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_script_url + .get_or_insert(Default::default()); + rule.set_level(rule.level().max(rule_severity.into())); + } "react/jsx-no-target-blank" => { if !options.include_inspired { results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Inspired); @@ -2721,6 +2769,18 @@ pub(crate) fn migrate_eslint_any_rule( .get_or_insert(Default::default()); rule.set_level(rule.level().max(rule_severity.into())); } + "solid/jsx-no-script-url" => { + 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_script_url + .get_or_insert(Default::default()); + rule.set_level(rule.level().max(rule_severity.into())); + } "solid/no-destructure" => { if !options.include_inspired { results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Inspired); diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index 802d93233676..3e1d311626f1 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -257,6 +257,7 @@ pub enum RuleName { NoRestrictedGlobals, NoRestrictedImports, NoRestrictedTypes, + NoScriptUrl, NoSecrets, NoSelfAssign, NoSelfCompare, @@ -658,6 +659,7 @@ impl RuleName { Self::NoRestrictedGlobals => "noRestrictedGlobals", Self::NoRestrictedImports => "noRestrictedImports", Self::NoRestrictedTypes => "noRestrictedTypes", + Self::NoScriptUrl => "noScriptUrl", Self::NoSecrets => "noSecrets", Self::NoSelfAssign => "noSelfAssign", Self::NoSelfCompare => "noSelfCompare", @@ -1055,6 +1057,7 @@ impl RuleName { Self::NoRestrictedGlobals => RuleGroup::Style, Self::NoRestrictedImports => RuleGroup::Style, Self::NoRestrictedTypes => RuleGroup::Style, + Self::NoScriptUrl => RuleGroup::Nursery, Self::NoSecrets => RuleGroup::Security, Self::NoSelfAssign => RuleGroup::Correctness, Self::NoSelfCompare => RuleGroup::Suspicious, @@ -1461,6 +1464,7 @@ impl std::str::FromStr for RuleName { "noRestrictedGlobals" => Ok(Self::NoRestrictedGlobals), "noRestrictedImports" => Ok(Self::NoRestrictedImports), "noRestrictedTypes" => Ok(Self::NoRestrictedTypes), + "noScriptUrl" => Ok(Self::NoScriptUrl), "noSecrets" => Ok(Self::NoSecrets), "noSelfAssign" => Ok(Self::NoSelfAssign), "noSelfCompare" => Ok(Self::NoSelfCompare), @@ -4820,7 +4824,7 @@ 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 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 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] = &[ @@ -4842,6 +4846,7 @@ impl Nursery { "noParametersOnlyUsedInRecursion", "noProto", "noReactForwardRef", + "noScriptUrl", "noShadow", "noSyncScripts", "noTernary", @@ -4887,7 +4892,8 @@ impl Nursery { ]; 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[48]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -4950,6 +4956,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57]), 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]), ]; } impl RuleGroupExt for Nursery { @@ -5051,216 +5058,221 @@ impl RuleGroupExt for Nursery { { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } - 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[18])); } - 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[19])); } - 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[20])); } - 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[21])); } - 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[22])); } - 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[23])); } - 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[24])); } - 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[25])); } - 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[26])); } - 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[27])); } - 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[28])); } - 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[29])); } - 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[30])); } - 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[31])); } - 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[32])); } - 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[33])); } - 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[34])); } - 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[35])); } - 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[36])); } - 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[37])); } - 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[38])); } - 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[39])); } - 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[40])); } - 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[41])); } - 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[42])); } - 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[43])); } - 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[44])); } - 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[45])); } - 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[46])); } - 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[47])); } - 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[48])); } - 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[49])); } - 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[50])); } - 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[51])); } - 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[52])); } - 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[53])); } - 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[54])); } - 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[55])); } - 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[56])); } - 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[57])); } - 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[58])); } - 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[59])); } + 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[60])); + } index_set } fn get_disabled_rules(&self) -> FxHashSet> { @@ -5355,216 +5367,221 @@ impl RuleGroupExt for Nursery { { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } - 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[18])); } - 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[19])); } - 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[20])); } - 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[21])); } - 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[22])); } - 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[23])); } - 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[24])); } - 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[25])); } - 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[26])); } - 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[27])); } - 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[28])); } - 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[29])); } - 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[30])); } - 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[31])); } - 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[32])); } - 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[33])); } - 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[34])); } - 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[35])); } - 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[36])); } - 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[37])); } - 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[38])); } - 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[39])); } - 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[40])); } - 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[41])); } - 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[42])); } - 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[43])); } - 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[44])); } - 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[45])); } - 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[46])); } - 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[47])); } - 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[48])); } - 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[49])); } - 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[50])); } - 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[51])); } - 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[52])); } - 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[53])); } - 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[54])); } - 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[55])); } - 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[56])); } - 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[57])); } - 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[58])); } - 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[59])); } + 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[60])); + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -5667,6 +5684,10 @@ impl RuleGroupExt for Nursery { .no_react_forward_ref .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noScriptUrl" => self + .no_script_url + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noShadow" => self .no_shadow .as_ref() @@ -5861,6 +5882,7 @@ impl From for Nursery { no_parameters_only_used_in_recursion: Some(value.into()), no_proto: Some(value.into()), no_react_forward_ref: Some(value.into()), + no_script_url: Some(value.into()), no_shadow: Some(value.into()), no_sync_scripts: Some(value.into()), no_ternary: Some(value.into()), diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 654e2a62845f..cd8771bf19eb 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -253,6 +253,7 @@ define_categories! { "lint/security/noDangerouslySetInnerHtml": "https://biomejs.dev/linter/rules/no-dangerously-set-inner-html", "lint/security/noDangerouslySetInnerHtmlWithChildren": "https://biomejs.dev/linter/rules/no-dangerously-set-inner-html-with-children", "lint/security/noGlobalEval": "https://biomejs.dev/linter/rules/no-global-eval", + "lint/nursery/noScriptUrl": "https://biomejs.dev/linter/rules/no-script-url", "lint/security/noSecrets": "https://biomejs.dev/linter/rules/no-secrets", "lint/style/noCommonJs": "https://biomejs.dev/linter/rules/no-common-js", "lint/style/noDefaultExport": "https://biomejs.dev/linter/rules/no-default-export", diff --git a/crates/biome_html_analyze/src/lint/nursery.rs b/crates/biome_html_analyze/src/lint/nursery.rs index 57159a909a01..ef733037648e 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_script_url; pub mod no_sync_scripts; pub mod no_vue_v_if_with_v_for; pub mod use_vue_hyphenated_attributes; @@ -13,4 +14,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_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_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_script_url.rs b/crates/biome_html_analyze/src/lint/nursery/no_script_url.rs new file mode 100644 index 000000000000..d844fd117921 --- /dev/null +++ b/crates/biome_html_analyze/src/lint/nursery/no_script_url.rs @@ -0,0 +1,106 @@ +use biome_analyze::{ + Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, +}; +use biome_console::markup; +use biome_diagnostics::Severity; +use biome_html_syntax::{AnyHtmlAttributeInitializer, HtmlOpeningElement, inner_string_text}; +use biome_rowan::{AstNode, TextRange}; +use biome_rule_options::no_script_url::NoScriptUrlOptions; +use biome_string_case::StrOnlyExtension; + +declare_lint_rule! { + /// Disallow `javascript:` URLs in HTML. + /// + /// Using `javascript:` URLs is considered a form of `eval` and can be a security risk. + /// These URLs can execute arbitrary JavaScript code, which can lead to cross-site scripting (XSS) vulnerabilities. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```html,expect_diagnostic + /// Click me + /// ``` + /// + /// ```html,expect_diagnostic + /// Click me + /// ``` + /// + /// ### Valid + /// + /// ```html + /// Click me + /// Click me + /// Click me + /// Not a real href + /// ``` + /// + pub NoScriptUrl { + version: "next", + name: "noScriptUrl", + language: "html", + // Show equivalents from related ecosystems + sources: &[ + RuleSource::Eslint("no-script-url").same(), + RuleSource::EslintReact("jsx-no-script-url").same(), + RuleSource::EslintQwik("jsx-no-script-url").same(), + RuleSource::EslintSolid("jsx-no-script-url").same(), + RuleSource::EslintReactXyz("dom-no-script-url").same(), + ], + recommended: true, + severity: Severity::Error, + } +} + +impl Rule for NoScriptUrl { + type Query = Ast; + type State = TextRange; + type Signals = Option; + type Options = NoScriptUrlOptions; + + fn run(ctx: &RuleContext) -> Option { + let element = ctx.query(); + + // Only check elements for HTML (unlike JSX where components/custom elements exist) + let name = element.name().ok()?; + let token = name.value_token().ok()?; + let tag = token.text_trimmed(); + if !tag.eq_ignore_ascii_case("a") { + return None; + } + + let attrs = element.attributes(); + let attr = attrs.find_by_name("href")?; + let initializer = attr.initializer()?; + let value = initializer.value().ok()?; + + if let AnyHtmlAttributeInitializer::HtmlString(html_string) = value + && let Ok(token) = html_string.value_token() + { + let inner = inner_string_text(&token); + if inner.trim().to_lowercase_cow().starts_with("javascript:") { + return Some(initializer.range()); + } + } + + None + } + + fn diagnostic(_ctx: &RuleContext, range: &Self::State) -> Option { + Some( + RuleDiagnostic::new( + rule_category!(), + *range, + markup! { + "Avoid using ""javascript:"" URLs, as they can be a security risk." + }, + ) + .note(markup! { + "Using ""javascript:"" URLs can lead to security vulnerabilities such as cross-site scripting (XSS)." + }) + .note(markup! { + "Consider using regular URLs, or if you need to handle click events, use event handlers instead." + }), + ) + } +} diff --git a/crates/biome_html_analyze/tests/spec_tests.rs b/crates/biome_html_analyze/tests/spec_tests.rs index a979683d675d..5eceac7540d2 100644 --- a/crates/biome_html_analyze/tests/spec_tests.rs +++ b/crates/biome_html_analyze/tests/spec_tests.rs @@ -13,8 +13,8 @@ use camino::Utf8Path; use std::ops::Deref; use std::{fs::read_to_string, slice}; -tests_macros::gen_tests! {"tests/specs/**/*.{html,vue,json,jsonc}", crate::run_test, "module"} -tests_macros::gen_tests! {"tests/suppression/**/*.{html,vue,json,jsonc}", crate::run_suppression_test, "module"} +tests_macros::gen_tests! {"tests/specs/**/*.{html,vue,astro,svelte,json,jsonc}", crate::run_test, "module"} +tests_macros::gen_tests! {"tests/suppression/**/*.{html,vue,astro,svelte,json,jsonc}", crate::run_suppression_test, "module"} fn run_test(input: &'static str, _: &str, _: &str, _: &str) { register_leak_checker(); diff --git a/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.astro b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.astro new file mode 100644 index 000000000000..077382fa6db5 --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.astro @@ -0,0 +1,7 @@ +--- +// Astro invalid cases - should trigger the rule +--- + +Void +Prompt +Uppercase diff --git a/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.astro.snap b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.astro.snap new file mode 100644 index 000000000000..30b86209b708 --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.astro.snap @@ -0,0 +1,71 @@ +--- +source: crates/biome_html_analyze/tests/spec_tests.rs +expression: invalid.astro +--- +# Input +```html +--- +// Astro invalid cases - should trigger the rule +--- + +Void +Prompt +Uppercase + +``` + +# Diagnostics +``` +invalid.astro:5:8 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Avoid using javascript: URLs, as they can be a security risk. + + 3 │ --- + 4 │ + > 5 │ Void + │ ^^^^^^^^^^^^^^^^^^^^^ + 6 │ Prompt + 7 │ Uppercase + + i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS). + + i Consider using regular URLs, or if you need to handle click events, use event handlers instead. + + +``` + +``` +invalid.astro:6:8 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Avoid using javascript: URLs, as they can be a security risk. + + 5 │ Void + > 6 │ Prompt + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 7 │ Uppercase + 8 │ + + i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS). + + i Consider using regular URLs, or if you need to handle click events, use event handlers instead. + + +``` + +``` +invalid.astro:7:8 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Avoid using javascript: URLs, as they can be a security risk. + + 5 │ Void + 6 │ Prompt + > 7 │ Uppercase + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 8 │ + + i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS). + + i Consider using regular URLs, or if you need to handle click events, use event handlers instead. + + +``` diff --git a/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.html b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.html new file mode 100644 index 000000000000..2c25c53f23a3 --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.html @@ -0,0 +1,9 @@ + + +Link + +Link + +Link + +Link diff --git a/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.html.snap b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.html.snap new file mode 100644 index 000000000000..9b84c3d15018 --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.html.snap @@ -0,0 +1,93 @@ +--- +source: crates/biome_html_analyze/tests/spec_tests.rs +expression: invalid.html +--- +# Input +```html + + +Link + +Link + +Link + +Link + +``` + +# Diagnostics +``` +invalid.html:3:8 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Avoid using javascript: URLs, as they can be a security risk. + + 1 │ + 2 │ + > 3 │ Link + │ ^^^^^^^^^^^^^^^^^^^^^ + 4 │ + 5 │ Link + + i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS). + + i Consider using regular URLs, or if you need to handle click events, use event handlers instead. + + +``` + +``` +invalid.html:5:8 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Avoid using javascript: URLs, as they can be a security risk. + + 3 │ Link + 4 │ + > 5 │ Link + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ + 6 │ + 7 │ Link + + i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS). + + i Consider using regular URLs, or if you need to handle click events, use event handlers instead. + + +``` + +``` +invalid.html:7:8 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Avoid using javascript: URLs, as they can be a security risk. + + 5 │ Link + 6 │ + > 7 │ Link + │ ^^^^^^^^^^^^^^^^^^^^^^ + 8 │ + 9 │ Link + + i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS). + + i Consider using regular URLs, or if you need to handle click events, use event handlers instead. + + +``` + +``` +invalid.html:9:8 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Avoid using javascript: URLs, as they can be a security risk. + + 7 │ Link + 8 │ + > 9 │ Link + │ ^^^^^^^^^^^^^^^^^^^^^ + 10 │ + + i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS). + + i Consider using regular URLs, or if you need to handle click events, use event handlers instead. + + +``` diff --git a/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.svelte b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.svelte new file mode 100644 index 000000000000..2b1c9d105a77 --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.svelte @@ -0,0 +1,5 @@ + + +Void +Confirm +Uppercase diff --git a/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.svelte.snap b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.svelte.snap new file mode 100644 index 000000000000..c5f202eea123 --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.svelte.snap @@ -0,0 +1,69 @@ +--- +source: crates/biome_html_analyze/tests/spec_tests.rs +expression: invalid.svelte +--- +# Input +```html + + +Void +Confirm +Uppercase + +``` + +# Diagnostics +``` +invalid.svelte:3:8 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Avoid using javascript: URLs, as they can be a security risk. + + 1 │ + 2 │ + > 3 │ Void + │ ^^^^^^^^^^^^^^^^^^^^^ + 4 │ Confirm + 5 │ Uppercase + + i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS). + + i Consider using regular URLs, or if you need to handle click events, use event handlers instead. + + +``` + +``` +invalid.svelte:4:8 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Avoid using javascript: URLs, as they can be a security risk. + + 3 │ Void + > 4 │ Confirm + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 5 │ Uppercase + 6 │ + + i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS). + + i Consider using regular URLs, or if you need to handle click events, use event handlers instead. + + +``` + +``` +invalid.svelte:5:8 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Avoid using javascript: URLs, as they can be a security risk. + + 3 │ Void + 4 │ Confirm + > 5 │ Uppercase + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 6 │ + + i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS). + + i Consider using regular URLs, or if you need to handle click events, use event handlers instead. + + +``` diff --git a/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.vue b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.vue new file mode 100644 index 000000000000..6c931f04f6fd --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.vue @@ -0,0 +1,7 @@ + + + diff --git a/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.vue.snap b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.vue.snap new file mode 100644 index 000000000000..b0f116e1b99e --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/invalid.vue.snap @@ -0,0 +1,72 @@ +--- +source: crates/biome_html_analyze/tests/spec_tests.rs +expression: invalid.vue +--- +# Input +```html + + + + +``` + +# Diagnostics +``` +invalid.vue:4:10 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Avoid using javascript: URLs, as they can be a security risk. + + 3 │ + 8 │ + + i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS). + + i Consider using regular URLs, or if you need to handle click events, use event handlers instead. + + +``` diff --git a/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.astro b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.astro new file mode 100644 index 000000000000..24a0a10eb75e --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.astro @@ -0,0 +1,9 @@ +--- +// Astro valid cases - should NOT trigger the rule +--- + +Docs +Astro +Footer + +

Not a link

diff --git a/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.astro.snap b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.astro.snap new file mode 100644 index 000000000000..2b88a44cb149 --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.astro.snap @@ -0,0 +1,17 @@ +--- +source: crates/biome_html_analyze/tests/spec_tests.rs +expression: valid.astro +--- +# Input +```html +--- +// Astro valid cases - should NOT trigger the rule +--- + +Docs +Astro +Footer + +

Not a link

+ +``` diff --git a/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.html b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.html new file mode 100644 index 000000000000..ef36b48dbb9a --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.html @@ -0,0 +1,10 @@ + + +Link + +Link + +Link + + +Not applicable diff --git a/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.html.snap b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.html.snap new file mode 100644 index 000000000000..030078d7a82a --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.html.snap @@ -0,0 +1,19 @@ +--- +source: crates/biome_html_analyze/tests/spec_tests.rs +assertion_line: 83 +expression: valid.html +--- +# Input +```html + + +Link + +Link + +Link + + +Not applicable + +``` diff --git a/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.svelte b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.svelte new file mode 100644 index 000000000000..d34ab6ced697 --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.svelte @@ -0,0 +1,7 @@ + + +Home +Biome +Top + +
Not a link
diff --git a/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.svelte.snap b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.svelte.snap new file mode 100644 index 000000000000..4d50527f6b0c --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.svelte.snap @@ -0,0 +1,15 @@ +--- +source: crates/biome_html_analyze/tests/spec_tests.rs +expression: valid.svelte +--- +# Input +```html + + +Home +Biome +Top + +
Not a link
+ +``` diff --git a/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.vue b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.vue new file mode 100644 index 000000000000..405a51c75426 --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.vue @@ -0,0 +1,9 @@ + + + diff --git a/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.vue.snap b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.vue.snap new file mode 100644 index 000000000000..f7e692f99181 --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/noScriptUrl/valid.vue.snap @@ -0,0 +1,17 @@ +--- +source: crates/biome_html_analyze/tests/spec_tests.rs +expression: valid.vue +--- +# Input +```html + + + + +``` diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index 923891adad6d..7ec664d3d9cd 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -20,6 +20,7 @@ pub mod no_next_async_client_component; pub mod no_parameters_only_used_in_recursion; pub mod no_proto; pub mod no_react_forward_ref; +pub mod no_script_url; pub mod no_shadow; pub mod no_sync_scripts; pub mod no_ternary; @@ -49,4 +50,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_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_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_script_url.rs b/crates/biome_js_analyze/src/lint/nursery/no_script_url.rs new file mode 100644 index 000000000000..5016c9fd5e36 --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/no_script_url.rs @@ -0,0 +1,181 @@ +use crate::react::ReactCreateElementCall; +use crate::services::semantic::Semantic; +use biome_analyze::context::RuleContext; +use biome_analyze::{Rule, RuleDiagnostic, RuleSource, declare_lint_rule}; +use biome_console::markup; +use biome_diagnostics::Severity; +use biome_js_syntax::{AnyJsxAttributeName, JsCallExpression, JsxAttribute}; +use biome_rowan::{AstNode, TextRange, declare_node_union}; +use biome_rule_options::no_script_url::NoScriptUrlOptions; +use biome_string_case::StrOnlyExtension; + +declare_lint_rule! { + /// Disallow `javascript:` URLs. + /// + /// Using `javascript:` URLs is considered a form of `eval` and can be a security risk. + /// These URLs can execute arbitrary JavaScript code, which can lead to cross-site scripting (XSS) vulnerabilities. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```jsx,expect_diagnostic + /// Click me + /// ``` + /// + /// ```jsx,expect_diagnostic + /// Click me + /// ``` + /// + /// ```js,expect_diagnostic + /// React.createElement('a', { href: 'javascript:void(0)' }); + /// ``` + /// + /// ### Valid + /// + /// ```jsx + /// Click me + /// ``` + /// + /// ```jsx + /// Click me + /// ``` + /// + /// ```jsx + /// Click me + /// ``` + /// + pub NoScriptUrl { + version: "next", + name: "noScriptUrl", + language: "js", + sources: &[ + RuleSource::Eslint("no-script-url").same(), + // Framework-specific equivalents + RuleSource::EslintReact("jsx-no-script-url").same(), + RuleSource::EslintQwik("jsx-no-script-url").same(), + RuleSource::EslintSolid("jsx-no-script-url").same(), + RuleSource::EslintReactXyz("dom-no-script-url").same(), + ], + recommended: true, + severity: Severity::Error, + } +} + +declare_node_union! { + pub AnyJsElementWithHref = JsxAttribute | JsCallExpression +} + +pub enum NoScriptUrlState { + JsxAttribute(TextRange), + ReactProp(TextRange), +} + +impl NoScriptUrlState { + fn range(&self) -> TextRange { + match self { + Self::JsxAttribute(range) | Self::ReactProp(range) => *range, + } + } +} + +impl Rule for NoScriptUrl { + type Query = Semantic; + type State = NoScriptUrlState; + type Signals = Option; + type Options = NoScriptUrlOptions; + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + let model = ctx.model(); + + match node { + AnyJsElementWithHref::JsxAttribute(jsx_attribute) => { + // Check if this is an href attribute + let name = jsx_attribute.name().ok()?; + if let AnyJsxAttributeName::JsxName(jsx_name) = name { + if jsx_name.syntax().text_trimmed() != "href" { + return None; + } + } else { + return None; + } + + // Check if the value contains javascript: + let static_value = jsx_attribute.as_static_value()?; + if let Some(const_str) = static_value.as_string_constant() + && const_str + .trim() + .to_lowercase_cow() + .starts_with("javascript:") + { + return Some(NoScriptUrlState::JsxAttribute( + jsx_attribute.initializer()?.range(), + )); + } + } + AnyJsElementWithHref::JsCallExpression(call_expression) => { + // Check if this is a React.createElement call + if let Some(react_create_element) = + ReactCreateElementCall::from_call_expression(call_expression, model) + { + let ReactCreateElementCall { props, .. } = react_create_element; + + // Look for href property in the props object + if let Some(props) = props { + let members = props.members(); + for member in members { + let Ok(member) = member else { continue }; + let Some(property_member) = member.as_js_property_object_member() + else { + continue; + }; + let Ok(property_name) = property_member.name() else { + continue; + }; + let Some(name) = property_name.as_js_literal_member_name() else { + continue; + }; + + if name.syntax().text_trimmed() == "href" { + let value = property_member.value().ok()?; + + // Check if it's a string literal with javascript: + if let Some(string_literal) = value.as_any_js_literal_expression() + && let Some(string_value) = + string_literal.as_js_string_literal_expression() + { + let text = string_value.inner_string_text().ok()?; + if text.trim().to_lowercase_cow().starts_with("javascript:") { + return Some(NoScriptUrlState::ReactProp(value.range())); + } + } + } + } + } + } + } + } + + None + } + + fn diagnostic(_ctx: &RuleContext, state: &Self::State) -> Option { + let diagnostic = RuleDiagnostic::new( + rule_category!(), + state.range(), + markup! { + "Avoid using ""javascript:"" URLs, as they can be a security risk." + } + .to_owned(), + ) + .note(markup! { + "Using ""javascript:"" URLs can lead to security vulnerabilities such as cross-site scripting (XSS)." + }) + .note(markup! { + "Consider using regular URLs, or if you need to handle click events, use event handlers instead." + }); + + Some(diagnostic) + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noScriptUrl/invalid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noScriptUrl/invalid.jsx new file mode 100644 index 000000000000..0d513ddf6a16 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noScriptUrl/invalid.jsx @@ -0,0 +1,15 @@ +// Invalid cases - should trigger the rule + +Link; + +Link; + +Link; + +Link; + +Link; + +React.createElement('a', { href: 'javascript:void(0)' }); + +React.createElement('a', { href: 'javascript:alert("XSS")' }); diff --git a/crates/biome_js_analyze/tests/specs/nursery/noScriptUrl/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noScriptUrl/invalid.jsx.snap new file mode 100644 index 000000000000..6bb1bca295bd --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noScriptUrl/invalid.jsx.snap @@ -0,0 +1,157 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 151 +expression: invalid.jsx +--- +# Input +```jsx +// Invalid cases - should trigger the rule + +Link; + +Link; + +Link; + +Link; + +Link; + +React.createElement('a', { href: 'javascript:void(0)' }); + +React.createElement('a', { href: 'javascript:alert("XSS")' }); + +``` + +# Diagnostics +``` +invalid.jsx:3:8 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Avoid using javascript: URLs, as they can be a security risk. + + 1 │ // Invalid cases - should trigger the rule + 2 │ + > 3 │ Link; + │ ^^^^^^^^^^^^^^^^^^^^^ + 4 │ + 5 │ Link; + + i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS). + + i Consider using regular URLs, or if you need to handle click events, use event handlers instead. + + +``` + +``` +invalid.jsx:5:8 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Avoid using javascript: URLs, as they can be a security risk. + + 3 │ Link; + 4 │ + > 5 │ Link; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ + 6 │ + 7 │ Link; + + i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS). + + i Consider using regular URLs, or if you need to handle click events, use event handlers instead. + + +``` + +``` +invalid.jsx:7:8 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Avoid using javascript: URLs, as they can be a security risk. + + 5 │ Link; + 6 │ + > 7 │ Link; + │ ^^^^^^^^^^^^^^^^^^^^^^ + 8 │ + 9 │ Link; + + i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS). + + i Consider using regular URLs, or if you need to handle click events, use event handlers instead. + + +``` + +``` +invalid.jsx:9:8 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Avoid using javascript: URLs, as they can be a security risk. + + 7 │ Link; + 8 │ + > 9 │ Link; + │ ^^^^^^^^^^^^^^^^^^^^^^ + 10 │ + 11 │ Link; + + i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS). + + i Consider using regular URLs, or if you need to handle click events, use event handlers instead. + + +``` + +``` +invalid.jsx:11:8 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Avoid using javascript: URLs, as they can be a security risk. + + 9 │ Link; + 10 │ + > 11 │ Link; + │ ^^^^^^^^^^^^^^^^^^^^^ + 12 │ + 13 │ React.createElement('a', { href: 'javascript:void(0)' }); + + i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS). + + i Consider using regular URLs, or if you need to handle click events, use event handlers instead. + + +``` + +``` +invalid.jsx:13:34 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Avoid using javascript: URLs, as they can be a security risk. + + 11 │ Link; + 12 │ + > 13 │ React.createElement('a', { href: 'javascript:void(0)' }); + │ ^^^^^^^^^^^^^^^^^^^^ + 14 │ + 15 │ React.createElement('a', { href: 'javascript:alert("XSS")' }); + + i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS). + + i Consider using regular URLs, or if you need to handle click events, use event handlers instead. + + +``` + +``` +invalid.jsx:15:34 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Avoid using javascript: URLs, as they can be a security risk. + + 13 │ React.createElement('a', { href: 'javascript:void(0)' }); + 14 │ + > 15 │ React.createElement('a', { href: 'javascript:alert("XSS")' }); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ + 16 │ + + i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS). + + i Consider using regular URLs, or if you need to handle click events, use event handlers instead. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noScriptUrl/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noScriptUrl/valid.jsx new file mode 100644 index 000000000000..be6cbed9ad8f --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noScriptUrl/valid.jsx @@ -0,0 +1,23 @@ +/* should not generate diagnostics */ + +// Valid cases - should not trigger the rule + +Link; + +Link; + +Link; + +Link; + +Link; + +Link; + +; + +React.createElement('a', { href: 'https://example.com' }); + +React.createElement('a', { href: '/path' }); + +React.createElement('a', { href: '#section' }); diff --git a/crates/biome_js_analyze/tests/specs/nursery/noScriptUrl/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noScriptUrl/valid.jsx.snap new file mode 100644 index 000000000000..59f043e65894 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noScriptUrl/valid.jsx.snap @@ -0,0 +1,31 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.jsx +--- +# Input +```jsx +/* should not generate diagnostics */ + +// Valid cases - should not trigger the rule + +Link; + +Link; + +Link; + +Link; + +Link; + +Link; + +; + +React.createElement('a', { href: 'https://example.com' }); + +React.createElement('a', { href: '/path' }); + +React.createElement('a', { href: '#section' }); + +``` diff --git a/crates/biome_rule_options/src/lib.rs b/crates/biome_rule_options/src/lib.rs index b113636debf2..721f83798e2b 100644 --- a/crates/biome_rule_options/src/lib.rs +++ b/crates/biome_rule_options/src/lib.rs @@ -170,6 +170,7 @@ pub mod no_restricted_elements; pub mod no_restricted_globals; pub mod no_restricted_imports; pub mod no_restricted_types; +pub mod no_script_url; pub mod no_secrets; pub mod no_self_assign; pub mod no_self_compare; diff --git a/crates/biome_rule_options/src/no_script_url.rs b/crates/biome_rule_options/src/no_script_url.rs new file mode 100644 index 000000000000..377ab9120dfb --- /dev/null +++ b/crates/biome_rule_options/src/no_script_url.rs @@ -0,0 +1,6 @@ +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 NoScriptUrlOptions {} diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index c0cbc55adf4d..b70818b696f9 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1947,6 +1947,11 @@ See */ noReactForwardRef?: NoReactForwardRefConfiguration; /** + * Disallow javascript: URLs in HTML. +See + */ + noScriptUrl?: NoScriptUrlConfiguration; + /** * Disallow variable declarations from shadowing variables declared in the outer scope. See */ @@ -3638,6 +3643,9 @@ export type NoProtoConfiguration = export type NoReactForwardRefConfiguration = | RulePlainConfiguration | RuleWithNoReactForwardRefOptions; +export type NoScriptUrlConfiguration = + | RulePlainConfiguration + | RuleWithNoScriptUrlOptions; export type NoShadowConfiguration = | RulePlainConfiguration | RuleWithNoShadowOptions; @@ -5068,6 +5076,10 @@ export interface RuleWithNoReactForwardRefOptions { level: RulePlainConfiguration; options?: NoReactForwardRefOptions; } +export interface RuleWithNoScriptUrlOptions { + level: RulePlainConfiguration; + options?: NoScriptUrlOptions; +} export interface RuleWithNoShadowOptions { level: RulePlainConfiguration; options?: NoShadowOptions; @@ -6389,6 +6401,7 @@ export type NoNextAsyncClientComponentOptions = {}; export type NoParametersOnlyUsedInRecursionOptions = {}; export type NoProtoOptions = {}; export type NoReactForwardRefOptions = {}; +export type NoScriptUrlOptions = {}; export type NoShadowOptions = {}; export type NoSyncScriptsOptions = {}; export type NoTernaryOptions = {}; @@ -7303,6 +7316,7 @@ export type Category = | "lint/security/noDangerouslySetInnerHtml" | "lint/security/noDangerouslySetInnerHtmlWithChildren" | "lint/security/noGlobalEval" + | "lint/nursery/noScriptUrl" | "lint/security/noSecrets" | "lint/style/noCommonJs" | "lint/style/noDefaultExport" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 28e755222813..15ce072e8246 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -4201,6 +4201,13 @@ }, "additionalProperties": false }, + "NoScriptUrlConfiguration": { + "oneOf": [ + { "$ref": "#/$defs/RulePlainConfiguration" }, + { "$ref": "#/$defs/RuleWithNoScriptUrlOptions" } + ] + }, + "NoScriptUrlOptions": { "type": "object", "additionalProperties": false }, "NoSecretsConfiguration": { "oneOf": [ { "$ref": "#/$defs/RulePlainConfiguration" }, @@ -5221,6 +5228,13 @@ { "type": "null" } ] }, + "noScriptUrl": { + "description": "Disallow javascript: URLs in HTML.\nSee ", + "anyOf": [ + { "$ref": "#/$defs/NoScriptUrlConfiguration" }, + { "type": "null" } + ] + }, "noShadow": { "description": "Disallow variable declarations from shadowing variables declared in the outer scope.\nSee ", "anyOf": [ @@ -7610,6 +7624,15 @@ "additionalProperties": false, "required": ["level"] }, + "RuleWithNoScriptUrlOptions": { + "type": "object", + "properties": { + "level": { "$ref": "#/$defs/RulePlainConfiguration" }, + "options": { "$ref": "#/$defs/NoScriptUrlOptions" } + }, + "additionalProperties": false, + "required": ["level"] + }, "RuleWithNoSecretsOptions": { "type": "object", "properties": {