-
-
Notifications
You must be signed in to change notification settings - Fork 991
feat(lint/js): add useVarsOnTop
#9861
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| --- | ||
| "@biomejs/biome": patch | ||
| --- | ||
|
|
||
| Added the new nursery rule [`useVarsOnTop`](https://biomejs.dev/linter/rules/use-vars-on-top/), which requires `var` declarations to appear at the top of their containing scope. | ||
|
|
||
| For example, the following code now triggers the rule: | ||
|
|
||
| ```js | ||
| function f() { | ||
| doSomething(); | ||
| var value = 1; | ||
| } | ||
| ``` | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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,237 @@ | ||
| use biome_analyze::{ | ||
| Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, | ||
| }; | ||
| use biome_console::markup; | ||
| use biome_diagnostics::Severity; | ||
| use biome_js_syntax::{ | ||
| AnyJsModuleItem, AnyJsStatement, JsExport, JsExpressionStatement, JsModule, JsModuleItemList, | ||
| JsStatementList, JsSyntaxKind, JsVariableDeclaration, JsVariableDeclarationClause, | ||
| JsVariableStatement, | ||
| }; | ||
| use biome_rowan::{AstNode, TextRange}; | ||
| use biome_rule_options::use_vars_on_top::UseVarsOnTopOptions; | ||
|
|
||
| declare_lint_rule! { | ||
| /// Require `var` declarations to appear at the top of their containing scope. | ||
| /// | ||
| /// Because `var` declarations are hoisted to the top of the nearest function, | ||
| /// script, module, or static block, placing them later in the body makes code | ||
| /// harder to follow. Keeping them at the top makes the scope's variable | ||
| /// declarations easier to find. Note that this is not a problem for `let` and | ||
| /// `const` declarations, which are block-scoped and not hoisted. | ||
| /// | ||
| /// This rule only allows leading standalone `var` statements. At module | ||
| /// scope, leading `export var` declarations are allowed too. Directives and | ||
| /// imports may appear before them. | ||
|
Comment on lines
+23
to
+25
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Docs/message drift from actual rule behaviour. The implementation permits a leading run of variable declarations (not just ✏️ Suggested wording update- /// This rule only allows leading standalone `var` statements. At module
- /// scope, leading `export var` declarations are allowed too. Directives and
- /// imports may appear before them.
+ /// This rule allows a leading run of variable declarations (`var`, `let`,
+ /// `const`). At module scope, leading variable exports are also allowed.
+ /// Directives and imports may appear before that leading run.
...
- "Move this "<Emphasis>"var"</Emphasis>" declaration before other statements in the same scope. At module scope, imports may remain before it."
+ "Move this "<Emphasis>"var"</Emphasis>" declaration before non-declaration statements in the same scope. At module scope, imports may remain before it."Based on learnings: Use Also applies to: 89-90 🤖 Prompt for AI Agents |
||
| /// | ||
| /// ## Examples | ||
| /// | ||
| /// ### Invalid | ||
| /// | ||
| /// ```js,expect_diagnostic | ||
| /// function f() { | ||
| /// doSomething(); | ||
| /// var value = 1; | ||
| /// } | ||
| /// ``` | ||
| /// | ||
| /// ### Valid | ||
| /// | ||
| /// ```js | ||
| /// function f() { | ||
| /// var value = 1; | ||
| /// doSomething(value); | ||
| /// } | ||
| /// ``` | ||
| /// | ||
| /// Related: | ||
| /// - [noVar](https://biomejs.dev/linter/rules/no-var/) | ||
| /// - [useConst](https://biomejs.dev/linter/rules/use-const/) | ||
| pub UseVarsOnTop { | ||
| version: "next", | ||
| name: "useVarsOnTop", | ||
| language: "js", | ||
| recommended: false, | ||
| sources: &[RuleSource::Eslint("vars-on-top").same()], | ||
| severity: Severity::Warning, | ||
| } | ||
| } | ||
|
|
||
| impl Rule for UseVarsOnTop { | ||
| type Query = Ast<JsVariableDeclaration>; | ||
| type State = TextRange; | ||
| type Signals = Option<Self::State>; | ||
| type Options = UseVarsOnTopOptions; | ||
|
|
||
| fn run(ctx: &RuleContext<Self>) -> Self::Signals { | ||
| let declaration = ctx.query(); | ||
|
|
||
| if !declaration.is_var() || is_var_on_top(declaration).unwrap_or_default() { | ||
| return None; | ||
| } | ||
|
|
||
| Some(declaration.range()) | ||
| } | ||
|
|
||
| fn diagnostic(_ctx: &RuleContext<Self>, range: &Self::State) -> Option<RuleDiagnostic> { | ||
| Some( | ||
| RuleDiagnostic::new( | ||
| rule_category!(), | ||
| *range, | ||
| markup! { | ||
| "This "<Emphasis>"var"</Emphasis>" declaration is not at the top of its containing scope." | ||
| }, | ||
| ) | ||
| .note(markup! { | ||
| <Emphasis>"var"</Emphasis>" declarations are hoisted to the top of their enclosing function, script, module, or static block, so declaring them later makes the scope harder to read." | ||
| }) | ||
| .note(markup! { | ||
| "Move this "<Emphasis>"var"</Emphasis>" declaration before other statements in the same scope. At module scope, imports may remain before it." | ||
| }), | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| fn is_var_on_top(declaration: &JsVariableDeclaration) -> Option<bool> { | ||
| let parent = declaration.syntax().parent()?; | ||
|
|
||
| if let Some(statement) = JsVariableStatement::cast(parent.clone()) { | ||
| return is_top_level_var_statement(&statement); | ||
| } | ||
|
|
||
| if let Some(clause) = JsVariableDeclarationClause::cast(parent.clone()) { | ||
| let parent = clause.syntax().parent()?; | ||
|
|
||
| if matches!( | ||
| parent.kind(), | ||
| JsSyntaxKind::TS_DECLARE_STATEMENT | JsSyntaxKind::TS_EXPORT_DECLARE_CLAUSE | ||
| ) { | ||
| return Some(true); | ||
| } | ||
|
|
||
| if let Some(export) = JsExport::cast(parent) { | ||
| return is_top_level_var_export(&export); | ||
| } | ||
| } | ||
|
|
||
| None | ||
| } | ||
|
|
||
| fn is_top_level_var_statement(statement: &JsVariableStatement) -> Option<bool> { | ||
| let parent = statement.syntax().parent()?; | ||
|
|
||
| if let Some(statements) = JsStatementList::cast(parent.clone()) { | ||
| let owner = statements.syntax().parent()?; | ||
|
|
||
| if !matches!( | ||
| owner.kind(), | ||
| JsSyntaxKind::JS_FUNCTION_BODY | ||
| | JsSyntaxKind::JS_SCRIPT | ||
| | JsSyntaxKind::JS_STATIC_INITIALIZATION_BLOCK_CLASS_MEMBER | ||
| ) { | ||
| return None; | ||
| } | ||
|
|
||
| return Some(is_top_statement_in_list(&statements, statement.syntax())); | ||
| } | ||
|
|
||
| if let Some(items) = JsModuleItemList::cast(parent) { | ||
| let owner = items.syntax().parent()?; | ||
|
|
||
| if !JsModule::can_cast(owner.kind()) { | ||
| return None; | ||
| } | ||
|
|
||
| return Some(is_top_module_item_in_list(&items, statement.syntax())); | ||
| } | ||
|
|
||
| None | ||
| } | ||
|
|
||
| fn is_top_level_var_export(export: &JsExport) -> Option<bool> { | ||
| let parent = export.syntax().parent()?; | ||
|
|
||
| let items = JsModuleItemList::cast(parent)?; | ||
| let owner = items.syntax().parent()?; | ||
|
|
||
| if !JsModule::can_cast(owner.kind()) { | ||
| return None; | ||
| } | ||
|
|
||
| Some(is_top_module_item_in_list(&items, export.syntax())) | ||
| } | ||
|
|
||
| fn is_top_statement_in_list( | ||
| statements: &JsStatementList, | ||
| target: &biome_js_syntax::JsSyntaxNode, | ||
| ) -> bool { | ||
| for statement in statements { | ||
| if statement.syntax() == target { | ||
| return true; | ||
| } | ||
|
|
||
| if !is_allowed_leading_statement(&statement) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| false | ||
| } | ||
|
|
||
| fn is_top_module_item_in_list( | ||
| items: &JsModuleItemList, | ||
| target: &biome_js_syntax::JsSyntaxNode, | ||
| ) -> bool { | ||
| for item in items { | ||
| if item.syntax() == target { | ||
|
dyc3 marked this conversation as resolved.
|
||
| return true; | ||
| } | ||
|
|
||
| let is_allowed = match item { | ||
| AnyJsModuleItem::JsImport(_) => true, | ||
| AnyJsModuleItem::AnyJsStatement(statement) => is_allowed_leading_statement(&statement), | ||
| AnyJsModuleItem::JsExport(export) => is_variable_export(&export), | ||
| }; | ||
|
|
||
| if !is_allowed { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| false | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| fn is_allowed_leading_statement(statement: &AnyJsStatement) -> bool { | ||
| is_directive_statement(statement) || is_variable_statement(statement) | ||
| } | ||
|
|
||
| fn is_directive_statement(statement: &AnyJsStatement) -> bool { | ||
| statement | ||
| .as_js_expression_statement() | ||
| .is_some_and(looks_like_directive) | ||
| } | ||
|
|
||
| fn looks_like_directive(statement: &JsExpressionStatement) -> bool { | ||
| statement | ||
| .expression() | ||
| .ok() | ||
| .and_then(|expression| expression.as_any_js_literal_expression().cloned()) | ||
| .is_some_and(|expression| expression.as_js_string_literal_expression().is_some()) | ||
| } | ||
|
|
||
| fn is_variable_statement(statement: &AnyJsStatement) -> bool { | ||
| matches!( | ||
| statement, | ||
| AnyJsStatement::JsVariableStatement(statement) if statement.declaration().is_ok() | ||
| ) | ||
| } | ||
|
|
||
| fn is_variable_export(export: &JsExport) -> bool { | ||
| export | ||
| .export_clause() | ||
| .ok() | ||
| .and_then(|clause| clause.as_any_js_declaration_clause().cloned()) | ||
| .and_then(|clause| clause.as_js_variable_declaration_clause().cloned()) | ||
| .and_then(|clause| clause.declaration().ok()) | ||
| .is_some() | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| /* should generate diagnostics */ | ||
| foo(); | ||
| var afterExpression = 1; | ||
|
|
||
| let okayBeforeExpression = 1; | ||
| foo(); | ||
| var afterLetAndExpression = 2; | ||
|
|
||
| if (condition) { | ||
| var insideBlock = 1; | ||
| } | ||
|
|
||
| for (var loopValue = 0; loopValue < 1; loopValue++) {} | ||
|
|
||
| function invalidFunction() { | ||
| "use strict"; | ||
| doSomething(); | ||
| var afterStatement = 1; | ||
| } | ||
|
|
||
| class InvalidExample { | ||
| static { | ||
| doSomething(); | ||
| var afterStaticStatement = 1; | ||
| } | ||
| } | ||
|
|
||
| export function helper() {} | ||
| export var exportedAfterStatement = 1; |
Uh oh!
There was an error while loading. Please reload this page.