Aptos Move Lint
The “Aptos Move Lint” tool runs on a Move package to find and warn about common issues in Move programs, helping improve your Move code.
You can run it with the aptos CLI command: aptos move lint
.
If you find any issues, please submit bugs and feedback. Also, we are tracking ideas and prioritization requests for new lint rules here, we welcome your contributions.
Lint Checks
Section titled “Lint Checks”aborting_overflow_checks
Section titled “aborting_overflow_checks”Checks for patterns that look like overflow checks done in a C style:
// Overflow checkif (x > x + y) { abort 1;};
// Underflow checkif (x < x - y) { abort 1;};
This pattern in Move does not make sense, as it either aborts immediately or is always true/false.
almost_swapped
Section titled “almost_swapped”Checks for expression patterns that look like a failed swap attempt and notifies the user. These patterns are likely erroneous code. This currently only detects simple access patterns such as assignments to a variable or a field of a struct. Examples include:
a = b; b = a;
which can be correctly swapped with(a, b) = (b, a);
a.x = b.x; b.x = a.x;
which can be correctly swapped with(a.x, b.x) = (b.x, a.x);
assert_const
Section titled “assert_const”Checks for trivial assertions, i.e. assert!(false)
and assert!(true)
. The latter is equivalent to a no-op, so can be removed entirely, while the former can be replaced by an abort.
avoid_copy_on_identity_comparison
Section titled “avoid_copy_on_identity_comparison”Checks for identity comparisons (==
or !=
) between copied values of type vector
or struct
(i.e., types for which copy can be expensive). It instead suggests to use reference-based identity comparison instead (i.e., use &x == &y
instead of x == y
, when the above mentioned conditions meet).
This recommendation is also given in the Move book. Due to automatic copy inference, it may not be obvious when a copy is being made while using ==
or !=
on values with types that have the copy
ability. This lint identifies cases where extra copies on vectors or structs could be skipped by using reference-based identity comparisons.
blocks_in_conditions
Section titled “blocks_in_conditions”Checks for use of blocks in conditions (e.g., in if
, match
, and while
conditions), which can make code hard to read. An example coding pattern caught by this lint is:
if ({let x = foo(); !x}) { // uses a block in condition bar();}
Such code can usually be rewritten to hoist the block out and above the condition, usually making it more readable.
It is a common Move pattern to provide inline specifications in conditions, especially loop invariants, which requires creating blocks in conditions. We exclude this pattern in the lint check to continue to allow for this specification pattern.
Note that an assert!
is translated to a conditional abort, so blocks in assert!
condition also are reported by this lint.
empty_if
Section titled “empty_if”Checks for if
statements that have no body, which is likely a mistake. For example:
if (x) {}
equal_operands_in_bin_op
Section titled “equal_operands_in_bin_op”Checks for binary operations where both operands are the same, which is likely a mistake. For example x % x
, x ^ x
, x > x
, x >= x
, x == x
, x | x
, x & x
, x / x
, and x != x
are all caught by this lint. The lint suggests replacing these with a more appropriate value or expression, such as 0
, true
, or false
.
This lint does not catch cases where the operands are vector access.
empty_range
Section titled “empty_range”Checks for empty ranges in for
loops, such as for i in 0..0 { ... }
, which do not execute the loop body. This can happen when the start of the range is greater than or equal to the end of the range, resulting in an empty iteration.
find_unnecessary_casts
Section titled “find_unnecessary_casts”Checks for unnecessary type casts where the source expression already has the same type as the target type. These casts are redundant and can be removed to improve code readability.
For example:
let x: u64 = 42;let y = x as u64; // unnecessary cast, x is already u64
The above can be simplified to:
let x: u64 = 42;let y = x; // cast removed
infinite_recursion
Section titled “infinite_recursion”Checks for functions or groups of functions that recurse infinitely. Note that the linter cannot detect conditional infinite recursion, only inconditional, so a flag is a definite error in the program.
known_to_abort
Section titled “known_to_abort”Checks for expressions that will always abort at runtime due to known constant values that violate runtime constraints. This lint helps identify code that will deterministically fail before it reaches production.
The following cases are detected:
- Bit shifts with excessive shift amounts:
x << n
orx >> n
wheren
is a constant that is greater than or equal to the bit width ofx
’s type. For example,value << 64
whenvalue
is of typeu64
will always abort. - Division or modulo by zero:
x / 0
orx % 0
operations will always abort at runtime. - Out-of-range type casting:
constant as type
where theconstant
value is outside the representable range of the targettype
. For example,300 as u8
will abort sinceu8
can only represent values 0-255.
needless_bool
Section titled “needless_bool”Checks for patterns of the form (where x
is any arbitrary boolean expression):
if (x) true else false
, which can be replaced with justx
.if (x) false else true
, which can be replaced with just!x
.if (x) { return true } else { return false }
, which can be replaced with justreturn x
.if (x) { return false } else { return true }
, which can be replaced with justreturn !x
.if (x) true else true
orif (x) false else false
, which should be rewritten to remove the redundant branch.
needless_deref_ref
Section titled “needless_deref_ref”Checks for patterns where references taken are immediately dereferenced, and suggests removing the pair of dereference-reference operators:
*&x.f
can be simplified tox.f
*&mut x.f
can be simplified tox.f
*&mut x.f = 5;
can be simplified tox.f = 5;
needless_loops
Section titled “needless_loops”Checks for loops that always exit on their first iteration, making the loop construct unnecessary. This lint detects loops that immediately return, abort, or break without performing any meaningful iteration.
The linter identifies these patterns:
- Loops where the first instruction is
return
,abort
, orbreak
- Loops where the first instruction is a conditional branch where both paths immediately exit the loop
Examples of needless loops:
// Always returns on first iterationloop { return 42;}
// Always aborts on first iterationloop { abort 1;}
These can be simplified by removing the loop construct entirely and using just the exit statement directly.
This lint uses conservative analysis to avoid false positives, only flagging cases where the exit behavior is immediately obvious from the loop’s structure.
needless_mutable_reference
Section titled “needless_mutable_reference”Checks for mutable references or borrows (currently: mutable reference parameters, mutable borrow of locals, borrow_global_mut
) that are not used mutably, and suggests to use the immutable reference or borrow instead.
For example, in the function foo
below, &mut
can be replaced by &
because the reference is not mutably used.
fun foo(x: u64): u64 { let y = &mut x; *y}
needless_ref_deref
Section titled “needless_ref_deref”Checks for patterns where immutable reference are taken for a dereference, and suggests removing the pair of reference-dereference operators: &*x
can be simplified to x
.
needless_ref_in_field_access
Section titled “needless_ref_in_field_access”Checks for patterns where there are needless references taken when accessing a field of a struct or an enum, and suggests removing the explicit reference taken:
(&s).f
can be simplified tos.f
(&mut s).f = 42;
can be simplified tos.f = 42;
needless_return
Section titled “needless_return”Checks for unnecessary return
statements in functions that can be simplified. This lint identifies cases where a return
statement is used to return a value that can be directly returned without the return
keyword.
For example, the following function:
public fun foo(): bool { // ... return true;}
This pattern can be simplified to:
public fun foo(): bool { // ... true}
nested_if
Section titled “nested_if”Checks for nested if statements that can be simplified using the &&
operator. This lint identifies patterns where an inner if statement with no else branch is contained within an outer if statement that also has no else branch.
if (a) { if (b) { // some code }}
This pattern can be simplified to:
if (a && b) { // some code}
The simplified version is more readable and avoids unnecessary nesting while maintaining the same logical behavior.
nonminimal_bool
Section titled “nonminimal_bool”Check for boolean expressions that can be simplified when a boolean literal (either true
or false
) is part of a binary or unary boolean operator. Examples:
x && true
is logically equivalent tox
x || true
is logically equivalent totrue
x => false
is logically equivalent to!x
x <==> true
is logically equivalent tox
! true
is logically equivalent tofalse
Does NOT consider side-effects/short-circuiting in the recommended simplifications. Example:
1/0 || true
is logically equivalent totrue
, but applying this simplification affects program semantics.
null_effects
Section titled “null_effects”Checks for statements that can be removed without changing program behavior. Examples:
42;
*(&mut 0) = /*...*/;
pure_function(21);
It also checks for more complex cases, such as
{ let x = 0; x += 1; x};
and
{ let x = 0; function_that_modifies_its_argument(&mut x); x};
self_assignment
Section titled “self_assignment”Checks for patterns where a variable or a field of a struct is assigned to itself and suggests removing the assignment. These assignments do not affect the state of the program. Examples include:
let x = x;
x = x;
a.x = a.x;
simpler_bool_expression
Section titled “simpler_bool_expression”Checks for boolean patterns that can be simplified through different boolean algebra laws. Examples include:
- Absorption law:
a && b || a
can be simplified toa
a || a && b
can be simplified toa
- Idempotence:
a && a
can be simplified toa
a || a
can be simplified toa
- Contradiction
a && !a
can be simplified tofalse
!a && a
can be simplified tofalse
- Tautology:
a || !a
can be simplified totrue
!a || a
can be simplified totrue
- Distributive law:
(a && b) || (a && c)
can be simplified toa && (b || c)
(a || b) && (a || c)
can be simplified toa || (b && c)
Where a
, b
and c
can either be simple or composed expressions.
simpler_numeric_expression
Section titled “simpler_numeric_expression”Checks for various patterns where a simpler numeric expression can be used instead. In all these cases, the code must already type check, and x
can be any numeric expression.
x & 0
,x * 0
,0 & x
,0 * x
,0 << x
,0 >> x
,x % 1
can all be replaced with just0
.x | 0
,x ^ 0
,x >> 0
,x << 0
,x + 0
,x - 0
,x / 1
,x * 1
,0 | x
,0 ^ x
,0 + x
,1 * x
can all be replaced with justx
.
unnecessary_boolean_identity_comparison
Section titled “unnecessary_boolean_identity_comparison”Checks for boolean identity comparisons of the form:
x == true
,true == x
, which can be replaced with justx
.x == false
,false == x
, which can be replaced with just!x
.
In all these cases, x
can be any arbitrary boolean expression.
unnecessary_numerical_extreme_comparison
Section titled “unnecessary_numerical_extreme_comparison”Checks if there are any numerical comparisons with extreme values (i.e., min and max value representable by that numeric type) that are unnecessary or can be made more precise and clear. Depending on the comparison, various recommendations are made.
Consider the following example expressions that are caught by the lint, and the corresponding recommendations made (in all these cases, x
is a place holder for a numerical expression of type u8
, u16
, u32
, u64
, u128
, or u256
, and MAX
is a place holder for the max value representable for that numeric type):
x < 0
,0 > x
,x > MAX
,MAX < x
, are always false, rewrite code to remove this comparisonx >= 0
,0 <= x
,x <= MAX
,MAX >= x
, are always true, rewrite code to remove this comparisonx <= 0
,0 >= x
,x >= MAX
,MAX <= x
, can all be simplified to use==
insteadx > 0
,0 < x
,x < MAX
,MAX > x
, can all be clarified to use!=
instead
redundant_comparison
Section titled “redundant_comparison”Checks for redundant, contradictory, and tautological numerical comparisons over the same variable in boolean expressions combined with &&
and ||
. This lint identifies logic errors in conditional statements that can lead to unreachable code or always-true/false conditions.
Examples of patterns caught by this lint:
- Redundant with
&&
:x <= 400 && x < 500
- the second condition is redundant since it’s always satisfied when the first is true - Contradictory with
&&
:x <= 400 && x > 500
- these conditions can never both be true, making the expression always false - Redundant with
||
:x > 10 || x >= 5
- the first condition is redundant since the second covers more cases - Tautology with
||
:x < 5 || x >= 5
- this expression is always true for any value of x
The lint only triggers when comparisons are on the same variable and can detect constants on either side of the comparison operator.
while_true
Section titled “while_true”Checks for while (true) { .... }
patterns and suggests using the more explicit loop { .... }
construct instead.
Suppressing Lint Warnings
Section titled “Suppressing Lint Warnings”To suppress one or more lint checks named check1
, check2
, … (and so on), you can add the attribute #[lint::skip(check1, check2, ...)]
to a function or a module. The linter will then not perform the checks named check1
, check2
, … (and so on) for that function or module.
For example, the function below would usually get a warning from the linter about a needless_bool
, but due to the attribute on the function, the linter does not emit a warning.
#[lint::skip(needless_bool)]fun violation(): bool { if (foo()) true else false}
Security Checks
Section titled “Security Checks”contains_in_table
Section titled “contains_in_table”Checks for unsafe usage of table operations that could lead to runtime errors. This lint helps prevent common mistakes when working with tables by detecting:
-
Unsafe
table::borrow
calls: Borrowing from a table without first checking if the key exists usingtable::contains
. This can cause runtime errors if the key doesn’t exist. -
Unsafe
table::add
calls: Adding to a table without ensuring the key doesn’t already exist. This can cause runtime errors if the key already exists.
The lint analyzes control flow to understand when keys are known to exist or not exist based on table::contains
checks.
Examples of patterns caught by this lint:
// Unsafe borrow - no contains checktable::borrow(&table, key); // Error: might fail if key doesn't exist
// Unsafe add - no contains checktable::add(&mut table, key, value); // Error: might fail if key already exists
// Wrong key checkif (table::contains(&table, key1)) { table::borrow(&table, key2); // Error: checked key1 but borrowing key2}
// Wrong branch usageif (!table::contains(&table, key)) { table::borrow(&table, key); // Error: key is known NOT to exist}
Recommended solutions:
- For
table::borrow
: Usetable::contains
first to check if the key exists - For
table::add
: Usetable::upsert
instead, or check withtable::contains
that the key doesn’t exist first
zero_address
Section titled “zero_address”This security check detects the use of address
parameters in external functions that are not checked for being zero before being used. This could lead to loss of funds or other security issues.⏎