Skip to content

feat(lint): no-alert rule #6355

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jun 25, 2025
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

382 changes: 203 additions & 179 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ define_categories! {
"lint/style/useThrowNewError": "https://biomejs.dev/linter/rules/use-throw-new-error",
"lint/style/useThrowOnlyError": "https://biomejs.dev/linter/rules/use-throw-only-error",
"lint/style/useTrimStartEnd": "https://biomejs.dev/linter/rules/use-trim-start-end",
"lint/suspicious/noAlert": "https://biomejs.dev/linter/rules/no-alert",
"lint/suspicious/noApproximativeNumericConstant": "https://biomejs.dev/linter/rules/no-approximative-numeric-constant",
"lint/suspicious/noArrayIndexKey": "https://biomejs.dev/linter/rules/no-array-index-key",
"lint/suspicious/noAssignInExpressions": "https://biomejs.dev/linter/rules/no-assign-in-expressions",
Expand Down
3 changes: 2 additions & 1 deletion crates/biome_js_analyze/src/lint/suspicious.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! Generated file, do not edit by hand, see `xtask/codegen`
use biome_analyze::declare_lint_group;
pub mod no_alert;
pub mod no_approximative_numeric_constant;
pub mod no_array_index_key;
pub mod no_assign_in_expressions;
Expand Down Expand Up @@ -75,4 +76,4 @@ pub mod use_is_array;
pub mod use_namespace_keyword;
pub mod use_number_to_fixed_digits_argument;
pub mod use_strict_mode;
declare_lint_group! { pub Suspicious { name : "suspicious" , rules : [self :: no_approximative_numeric_constant :: NoApproximativeNumericConstant , self :: no_array_index_key :: NoArrayIndexKey , self :: no_assign_in_expressions :: NoAssignInExpressions , self :: no_async_promise_executor :: NoAsyncPromiseExecutor , self :: no_catch_assign :: NoCatchAssign , self :: no_class_assign :: NoClassAssign , self :: no_comment_text :: NoCommentText , self :: no_compare_neg_zero :: NoCompareNegZero , self :: no_confusing_labels :: NoConfusingLabels , self :: no_confusing_void_type :: NoConfusingVoidType , self :: no_console :: NoConsole , self :: no_const_enum :: NoConstEnum , self :: no_control_characters_in_regex :: NoControlCharactersInRegex , self :: no_debugger :: NoDebugger , self :: no_document_cookie :: NoDocumentCookie , self :: no_document_import_in_page :: NoDocumentImportInPage , self :: no_double_equals :: NoDoubleEquals , self :: no_duplicate_case :: NoDuplicateCase , self :: no_duplicate_class_members :: NoDuplicateClassMembers , self :: no_duplicate_else_if :: NoDuplicateElseIf , self :: no_duplicate_jsx_props :: NoDuplicateJsxProps , self :: no_duplicate_object_keys :: NoDuplicateObjectKeys , self :: no_duplicate_parameters :: NoDuplicateParameters , self :: no_duplicate_test_hooks :: NoDuplicateTestHooks , self :: no_empty_block_statements :: NoEmptyBlockStatements , self :: no_empty_interface :: NoEmptyInterface , self :: no_evolving_types :: NoEvolvingTypes , self :: no_explicit_any :: NoExplicitAny , self :: no_exports_in_test :: NoExportsInTest , self :: no_extra_non_null_assertion :: NoExtraNonNullAssertion , self :: no_fallthrough_switch_clause :: NoFallthroughSwitchClause , self :: no_focused_tests :: NoFocusedTests , self :: no_function_assign :: NoFunctionAssign , self :: no_global_assign :: NoGlobalAssign , self :: no_global_is_finite :: NoGlobalIsFinite , self :: no_global_is_nan :: NoGlobalIsNan , self :: no_head_import_in_document :: NoHeadImportInDocument , self :: no_implicit_any_let :: NoImplicitAnyLet , self :: no_import_assign :: NoImportAssign , self :: no_irregular_whitespace :: NoIrregularWhitespace , self :: no_label_var :: NoLabelVar , self :: no_misleading_character_class :: NoMisleadingCharacterClass , self :: no_misleading_instantiator :: NoMisleadingInstantiator , self :: no_misplaced_assertion :: NoMisplacedAssertion , self :: no_misrefactored_shorthand_assign :: NoMisrefactoredShorthandAssign , self :: no_octal_escape :: NoOctalEscape , self :: no_prototype_builtins :: NoPrototypeBuiltins , self :: no_react_specific_props :: NoReactSpecificProps , self :: no_redeclare :: NoRedeclare , self :: no_redundant_use_strict :: NoRedundantUseStrict , self :: no_self_compare :: NoSelfCompare , self :: no_shadow_restricted_names :: NoShadowRestrictedNames , self :: no_skipped_tests :: NoSkippedTests , self :: no_sparse_array :: NoSparseArray , self :: no_suspicious_semicolon_in_jsx :: NoSuspiciousSemicolonInJsx , self :: no_template_curly_in_string :: NoTemplateCurlyInString , self :: no_then_property :: NoThenProperty , self :: no_unsafe_declaration_merging :: NoUnsafeDeclarationMerging , self :: no_unsafe_negation :: NoUnsafeNegation , self :: no_var :: NoVar , self :: no_with :: NoWith , self :: use_adjacent_overload_signatures :: UseAdjacentOverloadSignatures , self :: use_await :: UseAwait , self :: use_default_switch_clause_last :: UseDefaultSwitchClauseLast , self :: use_error_message :: UseErrorMessage , self :: use_getter_return :: UseGetterReturn , self :: use_google_font_display :: UseGoogleFontDisplay , self :: use_guard_for_in :: UseGuardForIn , self :: use_is_array :: UseIsArray , self :: use_namespace_keyword :: UseNamespaceKeyword , self :: use_number_to_fixed_digits_argument :: UseNumberToFixedDigitsArgument , self :: use_strict_mode :: UseStrictMode ,] } }
declare_lint_group! { pub Suspicious { name : "suspicious" , rules : [self :: no_alert :: NoAlert , self :: no_approximative_numeric_constant :: NoApproximativeNumericConstant , self :: no_array_index_key :: NoArrayIndexKey , self :: no_assign_in_expressions :: NoAssignInExpressions , self :: no_async_promise_executor :: NoAsyncPromiseExecutor , self :: no_catch_assign :: NoCatchAssign , self :: no_class_assign :: NoClassAssign , self :: no_comment_text :: NoCommentText , self :: no_compare_neg_zero :: NoCompareNegZero , self :: no_confusing_labels :: NoConfusingLabels , self :: no_confusing_void_type :: NoConfusingVoidType , self :: no_console :: NoConsole , self :: no_const_enum :: NoConstEnum , self :: no_control_characters_in_regex :: NoControlCharactersInRegex , self :: no_debugger :: NoDebugger , self :: no_document_cookie :: NoDocumentCookie , self :: no_document_import_in_page :: NoDocumentImportInPage , self :: no_double_equals :: NoDoubleEquals , self :: no_duplicate_case :: NoDuplicateCase , self :: no_duplicate_class_members :: NoDuplicateClassMembers , self :: no_duplicate_else_if :: NoDuplicateElseIf , self :: no_duplicate_jsx_props :: NoDuplicateJsxProps , self :: no_duplicate_object_keys :: NoDuplicateObjectKeys , self :: no_duplicate_parameters :: NoDuplicateParameters , self :: no_duplicate_test_hooks :: NoDuplicateTestHooks , self :: no_empty_block_statements :: NoEmptyBlockStatements , self :: no_empty_interface :: NoEmptyInterface , self :: no_evolving_types :: NoEvolvingTypes , self :: no_explicit_any :: NoExplicitAny , self :: no_exports_in_test :: NoExportsInTest , self :: no_extra_non_null_assertion :: NoExtraNonNullAssertion , self :: no_fallthrough_switch_clause :: NoFallthroughSwitchClause , self :: no_focused_tests :: NoFocusedTests , self :: no_function_assign :: NoFunctionAssign , self :: no_global_assign :: NoGlobalAssign , self :: no_global_is_finite :: NoGlobalIsFinite , self :: no_global_is_nan :: NoGlobalIsNan , self :: no_head_import_in_document :: NoHeadImportInDocument , self :: no_implicit_any_let :: NoImplicitAnyLet , self :: no_import_assign :: NoImportAssign , self :: no_irregular_whitespace :: NoIrregularWhitespace , self :: no_label_var :: NoLabelVar , self :: no_misleading_character_class :: NoMisleadingCharacterClass , self :: no_misleading_instantiator :: NoMisleadingInstantiator , self :: no_misplaced_assertion :: NoMisplacedAssertion , self :: no_misrefactored_shorthand_assign :: NoMisrefactoredShorthandAssign , self :: no_octal_escape :: NoOctalEscape , self :: no_prototype_builtins :: NoPrototypeBuiltins , self :: no_react_specific_props :: NoReactSpecificProps , self :: no_redeclare :: NoRedeclare , self :: no_redundant_use_strict :: NoRedundantUseStrict , self :: no_self_compare :: NoSelfCompare , self :: no_shadow_restricted_names :: NoShadowRestrictedNames , self :: no_skipped_tests :: NoSkippedTests , self :: no_sparse_array :: NoSparseArray , self :: no_suspicious_semicolon_in_jsx :: NoSuspiciousSemicolonInJsx , self :: no_template_curly_in_string :: NoTemplateCurlyInString , self :: no_then_property :: NoThenProperty , self :: no_unsafe_declaration_merging :: NoUnsafeDeclarationMerging , self :: no_unsafe_negation :: NoUnsafeNegation , self :: no_var :: NoVar , self :: no_with :: NoWith , self :: use_adjacent_overload_signatures :: UseAdjacentOverloadSignatures , self :: use_await :: UseAwait , self :: use_default_switch_clause_last :: UseDefaultSwitchClauseLast , self :: use_error_message :: UseErrorMessage , self :: use_getter_return :: UseGetterReturn , self :: use_google_font_display :: UseGoogleFontDisplay , self :: use_guard_for_in :: UseGuardForIn , self :: use_is_array :: UseIsArray , self :: use_namespace_keyword :: UseNamespaceKeyword , self :: use_number_to_fixed_digits_argument :: UseNumberToFixedDigitsArgument , self :: use_strict_mode :: UseStrictMode ,] } }
187 changes: 187 additions & 0 deletions crates/biome_js_analyze/src/lint/suspicious/no_alert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
use crate::services::semantic::Semantic;
use biome_analyze::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule};
use biome_console::markup;
use biome_js_semantic::SemanticModel;
use biome_js_syntax::{
AnyJsExpression, AnyJsLiteralExpression, JsCallExpression, JsComputedMemberExpression,
JsStaticMemberExpression, global_identifier,
};
use biome_rowan::AstNode;

const FORBIDDEN_FUNCTIONS: &[&str] = &["alert", "confirm", "prompt"];
const GLOBAL_OBJECTS: &[&str] = &["window", "globalThis"];

declare_lint_rule! {
/// Disallow the use of `alert`, `confirm`, and `prompt`.
///
/// JavaScript's `alert`, `confirm`, and `prompt` functions are widely considered to be obtrusive
/// as UI elements and should be replaced by a more appropriate custom UI implementation.
/// Furthermore, `alert` is often used while debugging code, which should be removed before
/// deployment to production.
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// alert("here!");
/// ```
///
/// ```js,expect_diagnostic
/// confirm("Are you sure?");
/// ```
///
/// ```js,expect_diagnostic
/// prompt("What's your name?", "John Doe");
/// ```
///
/// ### Valid
///
/// ```js
/// customAlert("Something happened!");
/// ```
///
/// ```js
/// customConfirm("Are you sure?");
/// ```
///
/// ```js
/// customPrompt("Who are you?");
/// ```
///
/// ```js
/// function foo() {
/// const alert = myCustomLib.customAlert;
/// alert();
/// }
/// ```
pub NoAlert {
version: "next",
name: "noAlert",
language: "js",
sources: &[RuleSource::Eslint("no-alert")],
recommended: false,
}
}

impl Rule for NoAlert {
type Query = Semantic<JsCallExpression>;
type State = String;
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let call = ctx.query();
let model = ctx.model();
let callee = call.callee().ok()?;

check_expression(&callee, model)
}

fn diagnostic(ctx: &RuleContext<Self>, function_name: &Self::State) -> Option<RuleDiagnostic> {
let call = ctx.query();

Some(
RuleDiagnostic::new(
rule_category!(),
call.range(),
markup! {
"Unexpected "<Emphasis>{function_name}</Emphasis>
},
)
.note(markup! {
"The "<Emphasis>{function_name}</Emphasis>" function is considered to be obtrusive. Replace it with a custom UI implementation."
}),
)
}
}

fn check_expression(expr: &AnyJsExpression, model: &SemanticModel) -> Option<String> {
match expr {
AnyJsExpression::JsIdentifierExpression(_) => check_global_identifier(expr, model),
AnyJsExpression::JsStaticMemberExpression(member_expr) => {
check_static_member_expression(member_expr, model)
}
AnyJsExpression::JsComputedMemberExpression(computed_member_expr) => {
check_computed_member_expression(computed_member_expr, model)
}
AnyJsExpression::JsParenthesizedExpression(paren_expr) => {
let inner_expr = paren_expr.expression().ok()?;
check_expression(&inner_expr, model)
}
_ => None,
}
}

fn check_global_identifier(expr: &AnyJsExpression, model: &SemanticModel) -> Option<String> {
let (reference, name) = global_identifier(expr)?;
let name_text = name.text();

if is_forbidden_function(name_text) && model.binding(&reference).is_none() {
Some(name_text.to_string())
} else {
None
}
}

fn check_static_member_expression(
member_expr: &JsStaticMemberExpression,
model: &SemanticModel,
) -> Option<String> {
let object = member_expr.object().ok()?;
let (reference, object_name) = global_identifier(&object)?;
let object_name_text = object_name.text();

if is_global_object(object_name_text) && model.binding(&reference).is_none() {
let member_name = member_expr.member().ok()?;
let member_token = member_name.value_token().ok()?;
let member_name_text = member_token.text_trimmed();

if is_forbidden_function(member_name_text) {
Some(member_name_text.to_string())
} else {
None
}
} else {
None
}
}

fn check_computed_member_expression(
computed_member_expr: &JsComputedMemberExpression,
model: &SemanticModel,
) -> Option<String> {
let object = computed_member_expr.object().ok()?;
let (reference, object_name) = global_identifier(&object)?;
let object_name_text = object_name.text();

if is_global_object(object_name_text) && model.binding(&reference).is_none() {
let member_expr = computed_member_expr.member().ok()?;
if let AnyJsExpression::AnyJsLiteralExpression(
AnyJsLiteralExpression::JsStringLiteralExpression(string_literal),
) = member_expr
{
let string_token = string_literal.value_token().ok()?;
let string_text = string_token.text_trimmed();
let member_name = string_text.trim_matches('"').trim_matches('\'');

if is_forbidden_function(member_name) {
Some(member_name.to_string())
} else {
None
}
} else {
None
}
} else {
None
}
}

fn is_forbidden_function(name: &str) -> bool {
FORBIDDEN_FUNCTIONS.contains(&name)
}

fn is_global_object(name: &str) -> bool {
GLOBAL_OBJECTS.contains(&name)
Copy link
Contributor

@vladimir-ivanov vladimir-ivanov Jun 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

open question - should we care about cases like:
let anyAlias = window;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd vote for no, otherwise we could keep chasing an endless amount of cases that are not yet caught 😅

}
1 change: 1 addition & 0 deletions crates/biome_js_analyze/src/options.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions crates/biome_js_analyze/tests/specs/suspicious/noAlert/invalid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Direct function calls (should trigger the rule)
alert("here!");

confirm("Are you sure?");

prompt("What's your name?", "John Doe");

// Window object calls (should trigger the rule)
window.alert("hello");

window.confirm("proceed?");

window.prompt("enter name");

// Bracket notation calls (should trigger the rule)
window["alert"]("bracket notation");

// Expression calls (should trigger the rule)
(alert)("wrapped in parens");

// Nested in other expressions
if (confirm("really?")) {
console.log("yes");
}

const result = prompt("input:");

// Multiple calls
alert("first");
alert("second");
confirm("third");
Loading