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
14 changes: 14 additions & 0 deletions .changeset/fresh-coins-do.md
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:
Comment thread
dyc3 marked this conversation as resolved.

```js
function f() {
doSomething();
var value = 1;
}
```
13 changes: 12 additions & 1 deletion .claude/skills/lint-rule-development/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,14 +320,19 @@ type Query = Semantic<AnyFunctionLike>;

## High Quality Diagnostics

Diagnostics must convey these messages, in this order:
**VERY IMPORTANT**: Rule diagnostics MUST convey these messages, in this order:

1. What the problem is
2. Why it's a problem (motivation to fix the issue)
3. How to fix it (actionable advice)

If the rule has an `action()` to fix the issue, the 3rd message should go in the action's message. If not, it should go in the diagnostic's advice.

Diagnostics must remain focused on the specific issue that the rule is flagging. Avoid including superfluous details that aren't directly relevant to the problem, as this can overwhelm users and obscure the main point.
If a rule can flag multiple classes of the same category of issue, the diagnostic messages should be surgically customized to the specific issue being flagged, rather than using generic messages that apply to all cases. This ensures that users receive precise and relevant information about the problem and how to fix it.

### Examples

Good:
```
1. "Foo is not allowed here."
Expand All @@ -349,6 +354,12 @@ Bad:
// third message missing is bad, because it doesn't give users a clear path to fix the issue
```

```
1. "This var declaration is not at the top of its containing scope." // Good start, explains what the problem is
2. "Move standalone var declarations before other statements in the same function, script, module, or static block." // Doesn't explain why, only tells the action. The "why" must come second, after the what.
3. "At module scope, imports and leading "<Emphasis>"export var"</Emphasis>" declarations may appear before other statements." // Doesn't explain the action, just gives a superfluous detail about module scope.
```

## Tips

- New rules are always in the `nursery` group. No need to move them to another category.
Expand Down
12 changes: 12 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.

4 changes: 4 additions & 0 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

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.

3 changes: 2 additions & 1 deletion crates/biome_diagnostics_categories/src/categories.rs

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

237 changes: 237 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/use_vars_on_top.rs
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Docs/message drift from actual rule behaviour.

The implementation permits a leading run of variable declarations (not just var), but the doc block and note currently read as “only var before other statements”. Please align wording with behaviour to avoid confusing users.

✏️ 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 RuleSource::Eslint("rule-name").same() when porting an ESLint rule with matching behavior.

Also applies to: 89-90

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/nursery/use_vars_on_top.rs` around lines 23
- 25, Update the doc comments in use_vars_on_top.rs (the top-level rule comment
and the note later) to reflect that the implementation allows a leading run of
variable declarations (not just `var`) and adjust wording to say "variable
declarations (e.g., var/let/const)" or similar; also when porting the ESLint
source, use RuleSource::Eslint("use-vars-on-top").same() to indicate identical
behavior. Ensure you modify the doc block at the top of use_vars_on_top.rs and
the explanatory note (the two places referenced) and replace any “only `var`”
phrasing with the broader, accurate description.

///
/// ## 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 {
Comment thread
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
}
Comment thread
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;
Loading
Loading