Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 15 additions & 0 deletions .changeset/twenty-meals-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@biomejs/biome": minor
---

Added new lint rule [`noShadow`](http://biome.dev/linter/rules/no-shadow), a port of eslint's `no-shadow`.

This rule disallows variable declarations from shadowing variables declared in an outer scope. For example:

```js
const foo = 1;

function bar() {
const foo = 2; // This variable shadows the outer foo
}
```
11 changes: 11 additions & 0 deletions crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

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

196 changes: 108 additions & 88 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 @@ -177,6 +177,7 @@ define_categories! {
"lint/nursery/noRestrictedImports": "https://biomejs.dev/linter/rules/no-restricted-imports",
"lint/nursery/noRestrictedTypes": "https://biomejs.dev/linter/rules/no-restricted-types",
"lint/nursery/noSecrets": "https://biomejs.dev/linter/rules/no-secrets",
"lint/nursery/noShadow": "https://biomejs.dev/linter/rules/no-shadow",
"lint/nursery/noShorthandPropertyOverrides": "https://biomejs.dev/linter/rules/no-shorthand-property-overrides",
"lint/nursery/noStaticElementInteractions": "https://biomejs.dev/linter/rules/no-static-element-interactions",
"lint/nursery/noSubstr": "https://biomejs.dev/linter/rules/no-substr",
Expand Down
3 changes: 2 additions & 1 deletion crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub mod no_restricted_elements;
pub mod no_restricted_imports;
pub mod no_restricted_types;
pub mod no_secrets;
pub mod no_shadow;
pub mod no_static_element_interactions;
pub mod no_substr;
pub mod no_template_curly_in_string;
Expand Down Expand Up @@ -62,4 +63,4 @@ pub mod use_strict_mode;
pub mod use_symbol_description;
pub mod use_trim_start_end;
pub mod use_valid_autocomplete;
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_await_in_loop :: NoAwaitInLoop , self :: no_bitwise_operators :: NoBitwiseOperators , self :: no_common_js :: NoCommonJs , self :: no_constant_binary_expression :: NoConstantBinaryExpression , self :: no_destructured_props :: NoDestructuredProps , self :: no_document_cookie :: NoDocumentCookie , self :: no_document_import_in_page :: NoDocumentImportInPage , self :: no_duplicate_else_if :: NoDuplicateElseIf , self :: no_dynamic_namespace_import_access :: NoDynamicNamespaceImportAccess , self :: no_enum :: NoEnum , self :: no_exported_imports :: NoExportedImports , self :: no_floating_promises :: NoFloatingPromises , self :: no_global_dirname_filename :: NoGlobalDirnameFilename , self :: no_head_element :: NoHeadElement , self :: no_head_import_in_document :: NoHeadImportInDocument , self :: no_img_element :: NoImgElement , self :: no_import_cycles :: NoImportCycles , self :: no_irregular_whitespace :: NoIrregularWhitespace , self :: no_nested_ternary :: NoNestedTernary , self :: no_noninteractive_element_interactions :: NoNoninteractiveElementInteractions , self :: no_octal_escape :: NoOctalEscape , self :: no_process_env :: NoProcessEnv , self :: no_process_global :: NoProcessGlobal , self :: no_restricted_elements :: NoRestrictedElements , self :: no_restricted_imports :: NoRestrictedImports , self :: no_restricted_types :: NoRestrictedTypes , self :: no_secrets :: NoSecrets , self :: no_static_element_interactions :: NoStaticElementInteractions , self :: no_substr :: NoSubstr , self :: no_template_curly_in_string :: NoTemplateCurlyInString , self :: no_ts_ignore :: NoTsIgnore , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unwanted_polyfillio :: NoUnwantedPolyfillio , self :: no_useless_escape_in_regex :: NoUselessEscapeInRegex , self :: no_useless_escape_in_string :: NoUselessEscapeInString , self :: no_useless_string_raw :: NoUselessStringRaw , self :: no_useless_undefined :: NoUselessUndefined , self :: use_adjacent_overload_signatures :: UseAdjacentOverloadSignatures , self :: use_aria_props_supported_by_role :: UseAriaPropsSupportedByRole , self :: use_at_index :: UseAtIndex , self :: use_collapsed_if :: UseCollapsedIf , self :: use_component_export_only_modules :: UseComponentExportOnlyModules , self :: use_consistent_curly_braces :: UseConsistentCurlyBraces , self :: use_consistent_member_accessibility :: UseConsistentMemberAccessibility , self :: use_consistent_object_definition :: UseConsistentObjectDefinition , self :: use_explicit_type :: UseExplicitType , self :: use_exports_last :: UseExportsLast , self :: use_for_component :: UseForComponent , self :: use_google_font_display :: UseGoogleFontDisplay , self :: use_google_font_preconnect :: UseGoogleFontPreconnect , self :: use_guard_for_in :: UseGuardForIn , self :: use_iterable_callback_return :: UseIterableCallbackReturn , self :: use_numeric_separators :: UseNumericSeparators , self :: use_parse_int_radix :: UseParseIntRadix , self :: use_sorted_classes :: UseSortedClasses , self :: use_strict_mode :: UseStrictMode , self :: use_symbol_description :: UseSymbolDescription , self :: use_trim_start_end :: UseTrimStartEnd , self :: use_valid_autocomplete :: UseValidAutocomplete ,] } }
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_await_in_loop :: NoAwaitInLoop , self :: no_bitwise_operators :: NoBitwiseOperators , self :: no_common_js :: NoCommonJs , self :: no_constant_binary_expression :: NoConstantBinaryExpression , self :: no_destructured_props :: NoDestructuredProps , self :: no_document_cookie :: NoDocumentCookie , self :: no_document_import_in_page :: NoDocumentImportInPage , self :: no_duplicate_else_if :: NoDuplicateElseIf , self :: no_dynamic_namespace_import_access :: NoDynamicNamespaceImportAccess , self :: no_enum :: NoEnum , self :: no_exported_imports :: NoExportedImports , self :: no_floating_promises :: NoFloatingPromises , self :: no_global_dirname_filename :: NoGlobalDirnameFilename , self :: no_head_element :: NoHeadElement , self :: no_head_import_in_document :: NoHeadImportInDocument , self :: no_img_element :: NoImgElement , self :: no_import_cycles :: NoImportCycles , self :: no_irregular_whitespace :: NoIrregularWhitespace , self :: no_nested_ternary :: NoNestedTernary , self :: no_noninteractive_element_interactions :: NoNoninteractiveElementInteractions , self :: no_octal_escape :: NoOctalEscape , self :: no_process_env :: NoProcessEnv , self :: no_process_global :: NoProcessGlobal , self :: no_restricted_elements :: NoRestrictedElements , self :: no_restricted_imports :: NoRestrictedImports , self :: no_restricted_types :: NoRestrictedTypes , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_static_element_interactions :: NoStaticElementInteractions , self :: no_substr :: NoSubstr , self :: no_template_curly_in_string :: NoTemplateCurlyInString , self :: no_ts_ignore :: NoTsIgnore , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unwanted_polyfillio :: NoUnwantedPolyfillio , self :: no_useless_escape_in_regex :: NoUselessEscapeInRegex , self :: no_useless_escape_in_string :: NoUselessEscapeInString , self :: no_useless_string_raw :: NoUselessStringRaw , self :: no_useless_undefined :: NoUselessUndefined , self :: use_adjacent_overload_signatures :: UseAdjacentOverloadSignatures , self :: use_aria_props_supported_by_role :: UseAriaPropsSupportedByRole , self :: use_at_index :: UseAtIndex , self :: use_collapsed_if :: UseCollapsedIf , self :: use_component_export_only_modules :: UseComponentExportOnlyModules , self :: use_consistent_curly_braces :: UseConsistentCurlyBraces , self :: use_consistent_member_accessibility :: UseConsistentMemberAccessibility , self :: use_consistent_object_definition :: UseConsistentObjectDefinition , self :: use_explicit_type :: UseExplicitType , self :: use_exports_last :: UseExportsLast , self :: use_for_component :: UseForComponent , self :: use_google_font_display :: UseGoogleFontDisplay , self :: use_google_font_preconnect :: UseGoogleFontPreconnect , self :: use_guard_for_in :: UseGuardForIn , self :: use_iterable_callback_return :: UseIterableCallbackReturn , self :: use_numeric_separators :: UseNumericSeparators , self :: use_parse_int_radix :: UseParseIntRadix , self :: use_sorted_classes :: UseSortedClasses , self :: use_strict_mode :: UseStrictMode , self :: use_symbol_description :: UseSymbolDescription , self :: use_trim_start_end :: UseTrimStartEnd , self :: use_valid_autocomplete :: UseValidAutocomplete ,] } }
227 changes: 227 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/no_shadow.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
use biome_analyze::{
QueryMatch, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
};
use biome_console::markup;
use biome_js_semantic::{Binding, SemanticModel};
use biome_js_syntax::{
JsClassExpression, JsFunctionExpression, JsIdentifierBinding, JsVariableDeclarator,
TsIdentifierBinding, TsTypeAliasDeclaration,
};
use biome_rowan::{AstNode, SyntaxNodeCast, TokenText, declare_node_union};

use crate::services::semantic::SemanticServices;

declare_lint_rule! {
/// Disallow variable declarations from shadowing variables declared in the outer scope.
///
/// Shadowing is the process by which a local variable shares the same name as a variable in its containing scope. This can cause confusion while reading the code and make it impossible to access the global variable.
///
/// See also: [`noShadowRestrictedNames`](http://biome.dev/linter/rules/no-shadow-restricted-names)
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// const foo = "bar";
/// if (true) {
/// const foo = "baz";
/// }
/// ```
///
/// Variable declarations in functions can shadow variables in the outer scope:
///
/// ```js,expect_diagnostic
/// const foo = "bar";
/// const bar = function () {
/// const foo = 10;
/// }
/// ```
///
/// Function argument names can shadow variables in the outer scope:
///
/// ```js,expect_diagnostic
/// const foo = "bar";
/// function bar(foo) {
/// foo = 10;
/// }
/// ```
///
/// ### Valid
///
/// ```js
/// const foo = "bar";
/// if (true) {
/// const qux = "baz";
/// }
/// ```
///
pub NoShadow {
version: "next",
name: "noShadow",
Comment thread
ematipico marked this conversation as resolved.
language: "js",
recommended: false,
sources: &[
RuleSource::Eslint("no-shadow"),
// uncomment when we can handle the test cases from typescript-eslint
// RuleSource::EslintTypeScript("no-shadow"),
],
Comment on lines +65 to +68

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have excluded the unit tests from typescript-eslint, and therefore the rule source here, because of #5822.

}
}

pub struct ShadowedBinding {
/// The binding that is violating the rule.
binding: Binding,
/// The binding that is shadowed.
shadowed_binding: Binding,
}

impl Rule for NoShadow {
type Query = SemanticServices;
type State = ShadowedBinding;
type Signals = Box<[Self::State]>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let mut shadowed_bindings = Vec::new();
let model = ctx.query();

for binding in ctx.query().all_bindings() {
if let Some(shadowed_binding) = check_shadowing(model, binding) {
shadowed_bindings.push(shadowed_binding);
}
}

shadowed_bindings.into_boxed_slice()
}

fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
//
// Read our guidelines to write great diagnostics:
// https://docs.rs/biome_analyze/latest/biome_analyze/#what-a-rule-should-say-to-the-user
//
Some(
RuleDiagnostic::new(
rule_category!(),
state.binding.tree().range(),
markup! {
"This variable shadows another variable with the same name in the outer scope."
},
)
.detail(
state.shadowed_binding.tree().range(),
markup!(
"This is the shadowed variable, which is now inaccessible in the inner scope."
),
)
.note(markup! {
"Consider renaming this variable. It's easy to confuse the origin of variables if they share the same name."
}),
)
}
}

fn check_shadowing(model: &SemanticModel, binding: Binding) -> Option<ShadowedBinding> {
let name = get_binding_name(&binding)?;

for upper in binding.scope().ancestors().skip(1) {
if let Some(upper_binding) = upper.get_binding(name.clone()) {
if binding.syntax() == upper_binding.syntax() {
// a binding can't shadow itself
continue;
}
if is_on_initializer(&binding, &upper_binding) {
continue;
}
if is_redeclaration(model, &binding, &upper_binding) {
// redeclarations are not shadowing, they get caught by `noRedeclare`
continue;
}
if is_declaration(&binding)
&& is_declaration(&upper_binding)
&& upper_binding.syntax().text_range().start() >= binding.scope().range().end()
{
// the shadowed binding must be declared before the shadowing one
continue;
}
Comment on lines +140 to +146

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This check is probably the most dubious one here because I don't have a source for it. I'm not entirely sure which part of the JS spec would be relevant.

return Some(ShadowedBinding {
binding,
shadowed_binding: upper_binding,
});
}
}
None
}

fn get_binding_name(binding: &Binding) -> Option<TokenText> {
let node = binding.syntax();
if let Some(ident) = node.clone().cast::<JsIdentifierBinding>() {
let name = ident.name_token().ok()?;
return Some(name.token_text_trimmed());
}
if let Some(ident) = node.clone().cast::<TsIdentifierBinding>() {
let name = ident.name_token().ok()?;
return Some(name.token_text_trimmed());
}
None
}

declare_node_union! {
pub(crate) AnyIdentifiableExpression = JsFunctionExpression | JsClassExpression
}

/// Checks if a variable `a` is inside the initializer of variable `b`.
///
/// This is used to avoid false positives in cases like this:
/// ```js
/// const c = function c() {}
/// ```
///
/// But the rule should still trigger on these cases:
/// ```js
/// var a = function(a) {};
/// ```
///
/// ```js
/// var a = function() { function a() {} };
/// ```
fn is_on_initializer(a: &Binding, b: &Binding) -> bool {
if let Some(b_initializer_expression) = b
.tree()
.parent::<JsVariableDeclarator>()
.and_then(|d| d.initializer())
.and_then(|i| i.expression().ok())
{
if let Some(a_parent) = a.tree().parent::<AnyIdentifiableExpression>() {
if a_parent.syntax() == b_initializer_expression.syntax() {
return true;
}
}
}

false
}

/// Checks if the binding `a` is a redeclaration of the binding `b`. Assumes that both bindings have the same identifier name.
fn is_redeclaration(model: &SemanticModel, a: &Binding, b: &Binding) -> bool {
if !is_declaration(a) || !is_declaration(b) {
return false;
}

let a_hoisted_scope = model.scope_hoisted_to(a.syntax()).unwrap_or(a.scope());
let b_hoisted_scope = model.scope_hoisted_to(b.syntax()).unwrap_or(b.scope());
a_hoisted_scope == b_hoisted_scope
}

/// Whether the binding is a declaration or not.
///
/// Examples of declarations:
/// ```js
/// var a;
/// let b;
/// const c;
/// ```
fn is_declaration(binding: &Binding) -> bool {
binding.tree().parent::<JsVariableDeclarator>().is_some()
|| binding.tree().parent::<TsTypeAliasDeclaration>().is_some()
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ use biome_rowan::AstNode;
declare_lint_rule! {
/// Disallow identifiers from shadowing restricted names.
///
/// See also: [`noShadow`](http://biome.dev/linter/rules/no-shadow)
///
/// ## Examples
///
/// ### Invalid
Expand Down
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.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
function a(x) { var b = function c() { var x = 'foo'; }; }
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: invalid-01.js
---
# Input
```js
function a(x) { var b = function c() { var x = 'foo'; }; }

```

# Diagnostics
```
invalid-01.js:1:44 lint/nursery/noShadow ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i This variable shadows another variable with the same name in the outer scope.

> 1 │ function a(x) { var b = function c() { var x = 'foo'; }; }
│ ^
2 │

i This is the shadowed variable, which is now inaccessible in the inner scope.

> 1 │ function a(x) { var b = function c() { var x = 'foo'; }; }
│ ^
2 │

i Consider renaming this variable. It's easy to confuse the origin of variables if they share the same name.


```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
var a = (x) => { var b = () => { var x = 'foo'; }; }
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: invalid-02.js
---
# Input
```js
var a = (x) => { var b = () => { var x = 'foo'; }; }

```

# Diagnostics
```
invalid-02.js:1:38 lint/nursery/noShadow ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i This variable shadows another variable with the same name in the outer scope.

> 1 │ var a = (x) => { var b = () => { var x = 'foo'; }; }
│ ^
2 │

i This is the shadowed variable, which is now inaccessible in the inner scope.

> 1 │ var a = (x) => { var b = () => { var x = 'foo'; }; }
│ ^
2 │

i Consider renaming this variable. It's easy to confuse the origin of variables if they share the same name.


```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
function a(x) { var b = function () { var x = 'foo'; }; }
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: invalid-03.js
---
# Input
```js
function a(x) { var b = function () { var x = 'foo'; }; }

```

# Diagnostics
```
invalid-03.js:1:43 lint/nursery/noShadow ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i This variable shadows another variable with the same name in the outer scope.

> 1 │ function a(x) { var b = function () { var x = 'foo'; }; }
│ ^
2 │

i This is the shadowed variable, which is now inaccessible in the inner scope.

> 1 │ function a(x) { var b = function () { var x = 'foo'; }; }
│ ^
2 │

i Consider renaming this variable. It's easy to confuse the origin of variables if they share the same name.


```
Loading