Skip to content
This repository was archived by the owner on Aug 31, 2023. It is now read-only.

feat(rome_js_analyze): noUnusedVariable rule #2987

Merged
merged 6 commits into from
Aug 3, 2022
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
3 changes: 2 additions & 1 deletion crates/rome_js_analyze/src/semantic_analyzers/js.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
@@ -1,16 +1,16 @@
use crate::{semantic_services::Semantic, JsRuleAction};
use crate::{semantic_services::Semantic, utils::batch::JsBatchMutation, JsRuleAction};
use rome_analyze::{
context::RuleContext, declare_rule, ActionCategory, Rule, RuleCategory, RuleDiagnostic,
};
use rome_console::markup;
use rome_diagnostics::Applicability;
use rome_js_semantic::{AllReferencesExtensions, Reference};
use rome_js_syntax::{
JsAnyExpression, JsAnyLiteralExpression, JsAnyRoot, JsIdentifierBinding,
JsIdentifierExpression, JsLanguage, JsStringLiteralExpression, JsVariableDeclaration,
JsVariableDeclarator, JsVariableDeclaratorList, JsVariableStatement,
JsAnyExpression, JsAnyLiteralExpression, JsIdentifierBinding, JsIdentifierExpression,
JsStringLiteralExpression, JsVariableDeclaration, JsVariableDeclarator,
JsVariableDeclaratorList,
};
use rome_rowan::{AstNode, AstSeparatedList, BatchMutation, BatchMutationExt, SyntaxNodeCast};
use rome_rowan::{AstNode, BatchMutationExt, SyntaxNodeCast};

declare_rule! {
/// Disallow the use of constants which its value is the upper-case version of its name.
Expand Down Expand Up @@ -53,53 +53,6 @@ fn is_id_and_string_literal_inner_text_equal(
}
}

/// Removes the declarator, and:
/// 1 - removes the statement if the declaration only has one declarator;
/// 2 - removes commas around the declarator to keep the declaration list valid.
fn remove_declarator(
batch: &mut BatchMutation<JsLanguage, JsAnyRoot>,
declarator: &JsVariableDeclarator,
) -> Option<()> {
let list = declarator.parent::<JsVariableDeclaratorList>()?;
let declaration = list.parent::<JsVariableDeclaration>()?;

if list.syntax_list().len() == 1 {
let statement = declaration.parent::<JsVariableStatement>()?;
batch.remove_node(statement);
} else {
let mut elements = list.elements();

// Find the declarator we want to remove
// remove its trailing comma, if there is one
let mut previous_element = None;
for element in elements.by_ref() {
if let Ok(node) = element.node() {
if node == declarator {
batch.remove_node(node.clone());
if let Some(comma) = element.trailing_separator().ok().flatten() {
batch.remove_token(comma.clone());
}
break;
}
}
previous_element = Some(element);
}

// if it is the last declarator of the list
// removes the comma before this element
let is_last = elements.next().is_none();
if is_last {
if let Some(element) = previous_element {
if let Some(comma) = element.trailing_separator().ok().flatten() {
batch.remove_token(comma.clone());
}
}
}
}

Some(())
}

pub struct State {
literal: JsStringLiteralExpression,
references: Vec<Reference>,
Expand Down Expand Up @@ -161,7 +114,7 @@ impl Rule for NoShoutyConstants {

let mut batch = root.begin();

remove_declarator(&mut batch, ctx.query());
batch.remove_js_variable_declarator(ctx.query());

for reference in state.references.iter() {
let node = reference
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
use crate::{semantic_services::Semantic, utils::batch::JsBatchMutation, JsRuleAction};
use rome_analyze::{
context::RuleContext, declare_rule, ActionCategory, Rule, RuleCategory, RuleDiagnostic,
};
use rome_console::markup;
use rome_diagnostics::Applicability;
use rome_js_semantic::{AllReferencesExtensions, SemanticScopeExtensions};
use rome_js_syntax::{
JsFormalParameter, JsFunctionDeclaration, JsIdentifierBinding, JsSyntaxKind,
JsVariableDeclarator,
};
use rome_rowan::{AstNode, BatchMutationExt};

declare_rule! {
/// Disallow unused variables.
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// const a = 4;
/// ```
///
/// ```js,expect_diagnostic
/// let a = 4;
/// ```
///
/// ```js,expect_diagnostic
/// function foo() {
/// };
/// ```
///
/// ```js,expect_diagnostic
/// function foo(myVar) {
/// console.log('foo');
/// }
/// foo();
/// ```
///
/// ```js,expect_diagnostic
/// const foo = () => {
/// };
/// ```
///
/// ```js,expect_diagnostic
/// function foo() {
/// foo();
/// }
/// ```
///
/// ```js,expect_diagnostic
/// const foo = () => {
/// foo();
/// console.log(this);
/// };
/// ```
///
/// # Valid
///
/// ```js
/// function foo(b) {
/// console.log(b)
/// };
/// foo();
/// ```
pub(crate) NoUnusedVariables {
version: "0.9.0",
name: "noUnusedVariables",
recommended: true,
}
}

impl Rule for NoUnusedVariables {
const CATEGORY: RuleCategory = RuleCategory::Lint;

type Query = Semantic<JsIdentifierBinding>;
type State = ();
type Signals = Option<Self::State>;

fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
let binding = ctx.query();
let model = ctx.model();

let all_references = binding.all_references(model);

if all_references.count() == 0 {
Some(())
} else {
// We need to check if all uses of this binding are somehow recursive

let function_declaration_scope = binding
.parent::<JsFunctionDeclaration>()
.map(|declaration| declaration.scope(model));

let declarator = binding.parent::<JsVariableDeclarator>();

let mut references_outside = 0;
for r in binding.all_references(model) {
let reference_scope = r.scope();

// If this binding is a function, and all its references are "inside" this
// function, we can safely say that this function is not used
if function_declaration_scope
.as_ref()
.map(|s| s.is_ancestor_of(&reference_scope))
.unwrap_or(false)
{
continue;
}

// Another possibility is if all its references are "inside" the same declaration
if let Some(declarator) = declarator.as_ref() {
let node = declarator.syntax();
if r.node().ancestors().any(|n| n == *node) {
continue;
}
}

references_outside += 1;
break;
}

if references_outside == 0 {
Some(())
} else {
None
}
}
}

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

let symbol_type = match binding.syntax().parent().unwrap().kind() {
JsSyntaxKind::JS_FORMAL_PARAMETER => "parameter",
JsSyntaxKind::JS_FUNCTION_DECLARATION => "function",
_ => "variable",
};

let diag = RuleDiagnostic::warning(
binding.syntax().text_trimmed_range(),
markup! {
"This " {symbol_type} " is unused."
},
);

let diag = diag.footer_note(
markup! {"Unused variables usually are result of incomplete refactoring, typos and other source of bugs."},
);

Some(diag)
}

fn action(ctx: &RuleContext<Self>, _: &Self::State) -> Option<JsRuleAction> {
let root = ctx.root();
let binding = ctx.query();

let mut batch = root.begin();

// If this is a function, remove the whole declaration
if let Some(declaration) = binding.parent::<JsFunctionDeclaration>() {
batch.remove_node(declaration)
} else if let Some(variable_declarator) = binding.parent::<JsVariableDeclarator>() {
batch.remove_js_variable_declarator(&variable_declarator);
} else if let Some(formal_parameter) = binding.parent::<JsFormalParameter>() {
batch.remove_js_formal_parameter(&formal_parameter);
}

let symbol_type = match binding.syntax().parent().unwrap().kind() {
JsSyntaxKind::JS_FORMAL_PARAMETER => "parameter",
JsSyntaxKind::JS_FUNCTION_DECLARATION => "function",
_ => "variable",
};

Some(JsRuleAction {
category: ActionCategory::QuickFix,
applicability: Applicability::Unspecified,
Copy link
Contributor

Choose a reason for hiding this comment

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

Considering that the applicability is Unspecified, what does it mean for the end user? Can an user apply the change via CLI?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

what does it mean for the end user? Can an user apply the change via CLI?
No.

I am torn apart between Unspecifiedand MaybeIncorrect. I assumed that removing unused code is not the "best" solution.

Copy link
Contributor

Choose a reason for hiding this comment

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

The semantics of Applicability is something we'll need to review when we rework the diagnostics, the current version of the enum is inherited directly from rslint and indirectly rustc, but those tools are using text-based suggestions instead of tree-based.
In this case I think the Unspecified applicability comes from rustc, where the concept of "applicability" was added when the compiler already has a bunch of existing suggestions, so all those diagnostics were set to Unspecified in the refactor to be manually reviewed later on.
In our case though we should always document if a suggestion is safe to apply or not, so in practice the only variants of Applicability we should be using a MaybeIncorrect and Always (the difference is that Unspecified can never be applied automatically, while MaybeIncorrect can after the has the user has reviewed it)

message: markup! { "Remove this " {symbol_type} "." }.to_owned(),
mutation: batch,
})
}
}
1 change: 1 addition & 0 deletions crates/rome_js_analyze/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use rome_js_syntax::{JsAnyRoot, JsAnyStatement, JsLanguage, JsModuleItemList, Js
use rome_rowan::{AstNode, BatchMutation};
use std::borrow::Cow;

pub mod batch;
pub mod rename;
#[cfg(test)]
pub mod tests;
Expand Down
Loading