Skip to content

Commit e128ea9

Browse files
anthonyshewsiketyanarendjrematipicovladimir-ivanov
authored
feat(lint): no-alert rule (#6355)
Co-authored-by: Naoki Ikeguchi <[email protected]> Co-authored-by: Emanuele Stoppa <[email protected]> Co-authored-by: siketyan <[email protected]> Co-authored-by: arendjr <[email protected]> Co-authored-by: ematipico <[email protected]> Co-authored-by: vladimir-ivanov <[email protected]>
1 parent 5705f1a commit e128ea9

13 files changed

Lines changed: 807 additions & 179 deletions

File tree

.changeset/weak-kings-behave.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added a new nursery rule `noAlert` that disallows the use of `alert`, `confirm` and `prompt`.
6+
7+
The following code is deemed incorrect:
8+
9+
```js
10+
alert("here!");
11+
```

crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/analyzer/linter/rules.rs

Lines changed: 202 additions & 178 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_diagnostics_categories/src/categories.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ define_categories! {
291291
"lint/style/useThrowNewError": "https://biomejs.dev/linter/rules/use-throw-new-error",
292292
"lint/style/useThrowOnlyError": "https://biomejs.dev/linter/rules/use-throw-only-error",
293293
"lint/style/useTrimStartEnd": "https://biomejs.dev/linter/rules/use-trim-start-end",
294+
"lint/suspicious/noAlert": "https://biomejs.dev/linter/rules/no-alert",
294295
"lint/suspicious/noApproximativeNumericConstant": "https://biomejs.dev/linter/rules/no-approximative-numeric-constant",
295296
"lint/suspicious/noArrayIndexKey": "https://biomejs.dev/linter/rules/no-array-index-key",
296297
"lint/suspicious/noAssignInExpressions": "https://biomejs.dev/linter/rules/no-assign-in-expressions",

crates/biome_js_analyze/src/lint/suspicious.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! Generated file, do not edit by hand, see `xtask/codegen`
44
55
use biome_analyze::declare_lint_group;
6+
pub mod no_alert;
67
pub mod no_approximative_numeric_constant;
78
pub mod no_array_index_key;
89
pub mod no_assign_in_expressions;
@@ -75,4 +76,4 @@ pub mod use_is_array;
7576
pub mod use_namespace_keyword;
7677
pub mod use_number_to_fixed_digits_argument;
7778
pub mod use_strict_mode;
78-
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 ,] } }
79+
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 ,] } }
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
use crate::services::semantic::Semantic;
2+
use biome_analyze::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule};
3+
use biome_console::markup;
4+
use biome_js_semantic::SemanticModel;
5+
use biome_js_syntax::{
6+
AnyJsExpression, AnyJsLiteralExpression, JsCallExpression, JsComputedMemberExpression,
7+
JsStaticMemberExpression, global_identifier,
8+
};
9+
use biome_rowan::AstNode;
10+
11+
const FORBIDDEN_FUNCTIONS: &[&str] = &["alert", "confirm", "prompt"];
12+
const GLOBAL_OBJECTS: &[&str] = &["window", "globalThis"];
13+
14+
declare_lint_rule! {
15+
/// Disallow the use of `alert`, `confirm`, and `prompt`.
16+
///
17+
/// JavaScript's `alert`, `confirm`, and `prompt` functions are widely considered to be obtrusive
18+
/// as UI elements and should be replaced by a more appropriate custom UI implementation.
19+
/// Furthermore, `alert` is often used while debugging code, which should be removed before
20+
/// deployment to production.
21+
///
22+
/// ## Examples
23+
///
24+
/// ### Invalid
25+
///
26+
/// ```js,expect_diagnostic
27+
/// alert("here!");
28+
/// ```
29+
///
30+
/// ```js,expect_diagnostic
31+
/// confirm("Are you sure?");
32+
/// ```
33+
///
34+
/// ```js,expect_diagnostic
35+
/// prompt("What's your name?", "John Doe");
36+
/// ```
37+
///
38+
/// ### Valid
39+
///
40+
/// ```js
41+
/// customAlert("Something happened!");
42+
/// ```
43+
///
44+
/// ```js
45+
/// customConfirm("Are you sure?");
46+
/// ```
47+
///
48+
/// ```js
49+
/// customPrompt("Who are you?");
50+
/// ```
51+
///
52+
/// ```js
53+
/// function foo() {
54+
/// const alert = myCustomLib.customAlert;
55+
/// alert();
56+
/// }
57+
/// ```
58+
pub NoAlert {
59+
version: "next",
60+
name: "noAlert",
61+
language: "js",
62+
sources: &[RuleSource::Eslint("no-alert")],
63+
recommended: false,
64+
}
65+
}
66+
67+
impl Rule for NoAlert {
68+
type Query = Semantic<JsCallExpression>;
69+
type State = String;
70+
type Signals = Option<Self::State>;
71+
type Options = ();
72+
73+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
74+
let call = ctx.query();
75+
let model = ctx.model();
76+
let callee = call.callee().ok()?;
77+
78+
check_expression(&callee, model)
79+
}
80+
81+
fn diagnostic(ctx: &RuleContext<Self>, function_name: &Self::State) -> Option<RuleDiagnostic> {
82+
let call = ctx.query();
83+
84+
Some(
85+
RuleDiagnostic::new(
86+
rule_category!(),
87+
call.range(),
88+
markup! {
89+
"Unexpected "<Emphasis>{function_name}</Emphasis>
90+
},
91+
)
92+
.note(markup! {
93+
"The "<Emphasis>{function_name}</Emphasis>" function is considered to be obtrusive. Replace it with a custom UI implementation."
94+
}),
95+
)
96+
}
97+
}
98+
99+
fn check_expression(expr: &AnyJsExpression, model: &SemanticModel) -> Option<String> {
100+
match expr {
101+
AnyJsExpression::JsIdentifierExpression(_) => check_global_identifier(expr, model),
102+
AnyJsExpression::JsStaticMemberExpression(member_expr) => {
103+
check_static_member_expression(member_expr, model)
104+
}
105+
AnyJsExpression::JsComputedMemberExpression(computed_member_expr) => {
106+
check_computed_member_expression(computed_member_expr, model)
107+
}
108+
AnyJsExpression::JsParenthesizedExpression(paren_expr) => {
109+
let inner_expr = paren_expr.expression().ok()?;
110+
check_expression(&inner_expr, model)
111+
}
112+
_ => None,
113+
}
114+
}
115+
116+
fn check_global_identifier(expr: &AnyJsExpression, model: &SemanticModel) -> Option<String> {
117+
let (reference, name) = global_identifier(expr)?;
118+
let name_text = name.text();
119+
120+
if is_forbidden_function(name_text) && model.binding(&reference).is_none() {
121+
Some(name_text.to_string())
122+
} else {
123+
None
124+
}
125+
}
126+
127+
fn check_static_member_expression(
128+
member_expr: &JsStaticMemberExpression,
129+
model: &SemanticModel,
130+
) -> Option<String> {
131+
let object = member_expr.object().ok()?;
132+
let (reference, object_name) = global_identifier(&object)?;
133+
let object_name_text = object_name.text();
134+
135+
if is_global_object(object_name_text) && model.binding(&reference).is_none() {
136+
let member_name = member_expr.member().ok()?;
137+
let member_token = member_name.value_token().ok()?;
138+
let member_name_text = member_token.text_trimmed();
139+
140+
if is_forbidden_function(member_name_text) {
141+
Some(member_name_text.to_string())
142+
} else {
143+
None
144+
}
145+
} else {
146+
None
147+
}
148+
}
149+
150+
fn check_computed_member_expression(
151+
computed_member_expr: &JsComputedMemberExpression,
152+
model: &SemanticModel,
153+
) -> Option<String> {
154+
let object = computed_member_expr.object().ok()?;
155+
let (reference, object_name) = global_identifier(&object)?;
156+
let object_name_text = object_name.text();
157+
158+
if is_global_object(object_name_text) && model.binding(&reference).is_none() {
159+
let member_expr = computed_member_expr.member().ok()?;
160+
if let AnyJsExpression::AnyJsLiteralExpression(
161+
AnyJsLiteralExpression::JsStringLiteralExpression(string_literal),
162+
) = member_expr
163+
{
164+
let string_token = string_literal.value_token().ok()?;
165+
let string_text = string_token.text_trimmed();
166+
let member_name = string_text.trim_matches('"').trim_matches('\'');
167+
168+
if is_forbidden_function(member_name) {
169+
Some(member_name.to_string())
170+
} else {
171+
None
172+
}
173+
} else {
174+
None
175+
}
176+
} else {
177+
None
178+
}
179+
}
180+
181+
fn is_forbidden_function(name: &str) -> bool {
182+
FORBIDDEN_FUNCTIONS.contains(&name)
183+
}
184+
185+
fn is_global_object(name: &str) -> bool {
186+
GLOBAL_OBJECTS.contains(&name)
187+
}

crates/biome_js_analyze/src/options.rs

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Direct function calls (should trigger the rule)
2+
alert("here!");
3+
4+
confirm("Are you sure?");
5+
6+
prompt("What's your name?", "John Doe");
7+
8+
// Window object calls (should trigger the rule)
9+
window.alert("hello");
10+
11+
window.confirm("proceed?");
12+
13+
window.prompt("enter name");
14+
15+
// Bracket notation calls (should trigger the rule)
16+
window["alert"]("bracket notation");
17+
18+
// Expression calls (should trigger the rule)
19+
(alert)("wrapped in parens");
20+
21+
// Nested in other expressions
22+
if (confirm("really?")) {
23+
console.log("yes");
24+
}
25+
26+
const result = prompt("input:");
27+
28+
// Multiple calls
29+
alert("first");
30+
alert("second");
31+
confirm("third");

0 commit comments

Comments
 (0)