Skip to content

Commit

Permalink
[Linter] Unnecessary while loop (#16876)
Browse files Browse the repository at this point in the history
# Description
This linter encourages replacing `while(true)` loops with the more
idiomatic loop construct. Here's a breakdown of how it works:
It checks each expression in the AST.
If the expression is a While loop, it examines the condition.
If the condition is always true (using the `is_condition_always_true`
function), it reports a diagnostic suggesting to use loop instead.

The `is_condition_always_true` function checks if the condition is a
boolean literal with the value true.

## Test plan
Added more use case including false positive, false negative case

## Release notes

- [ ] Protocol: 
- [ ] Nodes (Validators and Full nodes): 
- [ ] Indexer: 
- [ ] JSON-RPC: 
- [ ] GraphQL: 
- [X] CLI: Move will now lint against `while (true)`, which should be
replaced by `loop`
- [ ] Rust SDK:

---------

Co-authored-by: jamedzung <[email protected]>
Co-authored-by: Todd Nowacki <[email protected]>
  • Loading branch information
3 people authored Aug 14, 2024
1 parent b490bb1 commit c4343a2
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 9 deletions.
32 changes: 23 additions & 9 deletions external-crates/move/crates/move-compiler/src/linters/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::{
linters::constant_naming::ConstantNamingVisitor, typing::visitor::TypingVisitor,
};
pub mod constant_naming;
mod unnecessary_while_loop;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LintLevel {
Expand All @@ -34,26 +35,39 @@ pub const ALLOW_ATTR_CATEGORY: &str = "lint";
pub const LINT_WARNING_PREFIX: &str = "Lint ";
pub const CONSTANT_NAMING_FILTER_NAME: &str = "constant_naming";
pub const CONSTANT_NAMING_DIAG_CODE: u8 = 1;
pub const WHILE_TRUE_TO_LOOP_FILTER_NAME: &str = "while_true";
pub const WHILE_TRUE_TO_LOOP_DIAG_CODE: u8 = 4;

pub fn known_filters() -> (Option<Symbol>, Vec<WarningFilter>) {
(
Some(ALLOW_ATTR_CATEGORY.into()),
vec![WarningFilter::code(
Some(LINT_WARNING_PREFIX),
LinterDiagnosticCategory::Style as u8,
CONSTANT_NAMING_DIAG_CODE,
Some(CONSTANT_NAMING_FILTER_NAME),
)],
vec![
WarningFilter::code(
Some(LINT_WARNING_PREFIX),
LinterDiagnosticCategory::Style as u8,
CONSTANT_NAMING_DIAG_CODE,
Some(CONSTANT_NAMING_FILTER_NAME),
),
WarningFilter::code(
Some(LINT_WARNING_PREFIX),
LinterDiagnosticCategory::Complexity as u8,
WHILE_TRUE_TO_LOOP_DIAG_CODE,
Some(WHILE_TRUE_TO_LOOP_FILTER_NAME),
),
],
)
}

pub fn linter_visitors(level: LintLevel) -> Vec<Visitor> {
match level {
LintLevel::None | LintLevel::Default => vec![],
LintLevel::All => {
vec![constant_naming::ConstantNamingVisitor::visitor(
ConstantNamingVisitor,
)]
vec![
constant_naming::ConstantNamingVisitor::visitor(ConstantNamingVisitor),
unnecessary_while_loop::WhileTrueToLoop::visitor(
unnecessary_while_loop::WhileTrueToLoop,
),
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//! Encourages replacing `while(true)` with `loop` for infinite loops in Move for clarity and conciseness.
//! Identifies `while(true)` patterns, suggesting a more idiomatic approach using `loop`.
//! Aims to enhance code readability and adherence to Rust idioms.
use crate::{
diag,
diagnostics::{
codes::{custom, DiagnosticInfo, Severity},
WarningFilters,
},
expansion::ast::Value_,
shared::CompilationEnv,
typing::{
ast::{self as T, UnannotatedExp_},
visitor::{TypingVisitorConstructor, TypingVisitorContext},
},
};

use super::{LinterDiagnosticCategory, LINT_WARNING_PREFIX, WHILE_TRUE_TO_LOOP_DIAG_CODE};

const WHILE_TRUE_TO_LOOP_DIAG: DiagnosticInfo = custom(
LINT_WARNING_PREFIX,
Severity::Warning,
LinterDiagnosticCategory::Complexity as u8,
WHILE_TRUE_TO_LOOP_DIAG_CODE,
"unnecessary 'while (true)', replace with 'loop'",
);

pub struct WhileTrueToLoop;

pub struct Context<'a> {
env: &'a mut CompilationEnv,
}

impl TypingVisitorConstructor for WhileTrueToLoop {
type Context<'a> = Context<'a>;

fn context<'a>(env: &'a mut CompilationEnv, _program: &T::Program) -> Self::Context<'a> {
Context { env }
}
}

impl TypingVisitorContext for Context<'_> {
fn add_warning_filter_scope(&mut self, filter: WarningFilters) {
self.env.add_warning_filter_scope(filter)
}
fn pop_warning_filter_scope(&mut self) {
self.env.pop_warning_filter_scope()
}

fn visit_exp_custom(&mut self, exp: &mut T::Exp) -> bool {
let UnannotatedExp_::While(_, cond, _) = &exp.exp.value else {
return false;
};
let UnannotatedExp_::Value(sp!(_, Value_::Bool(true))) = &cond.exp.value else {
return false;
};

let msg = "'while (true)' can be always replaced with 'loop'";
let mut diag = diag!(WHILE_TRUE_TO_LOOP_DIAG, (exp.exp.loc, msg));
diag.add_note(
"A 'loop' is more useful in these cases. Unlike 'while', 'loop' can have a \
'break' with a value, e.g. 'let x = loop { break 42 };'",
);
self.env.add_diag(diag);

false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module 0x42::loop_test {

// These should trigger but currently dont
public fun false_negative_obfuscated_true() {
let always_true = true;
while (always_true) {};
while (true && true) {};
while (true || false) {};
while (1 > 0) {};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module 0x42::loop_test {

#[allow(lint(while_true))]
public fun suppressed_while_true() {
while (true) {};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module 0x42::loop_test {

public fun true_negative_while_with_condition() {
let b = false;
while (false) {};
while (b) {};
while (false && true) {};
while (false || false) {};
while (0 > 1) {};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
warning[Lint W01004]: unnecessary 'while (true)', replace with 'loop'
┌─ tests/linter/true_positive_unnecessary_while_loop.move:3:9
3 │ while (true) {};
│ ^^^^^^^^^^^^^^^ 'while (true)' can be always replaced with 'loop'
= A 'loop' is more useful in these cases. Unlike 'while', 'loop' can have a 'break' with a value, e.g. 'let x = loop { break 42 };'
= This warning can be suppressed with '#[allow(lint(while_true))]' applied to the 'module' or module member ('const', 'fun', or 'struct')

warning[Lint W01004]: unnecessary 'while (true)', replace with 'loop'
┌─ tests/linter/true_positive_unnecessary_while_loop.move:4:9
4 │ while (true) { break }
│ ^^^^^^^^^^^^^^^^^^^^^^ 'while (true)' can be always replaced with 'loop'
= A 'loop' is more useful in these cases. Unlike 'while', 'loop' can have a 'break' with a value, e.g. 'let x = loop { break 42 };'
= This warning can be suppressed with '#[allow(lint(while_true))]' applied to the 'module' or module member ('const', 'fun', or 'struct')

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module 0x42::loop_test {
public fun true_positive_infinite_loop() {
while (true) {};
while (true) { break }
}
}

0 comments on commit c4343a2

Please sign in to comment.