Running Lints
The Move compiler ships with a set of lints - static checks that flag suspicious patterns in the code at compile time. Tests verify that the code does what it should; lints catch code that compiles and may even pass tests, but does something a more experienced Move developer would not write: transfers that break composability, comparisons that never do what they look like they do, or an entry function that can never be called. Running lints regularly - and keeping the package free of warnings - is a cheap way to maintain code quality.
Running Lints
The sui move lint command compiles the package and runs the full set of linters:
sui move lint
To also check the code in the tests directory, add the --test flag:
sui move lint --test
The same checks are available on other commands via the --lint flag - for example, sui move test --lint runs the tests and the full lint set in one go.
Consider a module with a function that transfers a newly created object to the transaction sender:
module book::mint;
public struct Item has key, store { id: UID }
public fun mint(ctx: &mut TxContext) {
let item = Item { id: object::new(ctx) };
transfer::transfer(item, ctx.sender());
}
Running the linter prints a warning with an explanation and a pointer to the exact expression:
warning[Lint W99001]: non-composable transfer to sender
┌─ ./sources/mint.move:7:5
│
5 │ public fun mint(ctx: &mut TxContext) {
│ ---- Returning an object from a function, allows a caller to use the object and enables composability via programmable transactions.
6 │ let item = Item { id: object::new(ctx) };
7 │ transfer::transfer(item, ctx.sender());
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
│ │ │
│ │ Transaction sender address coming from here
│ Transfer of an object to transaction sender address
│
= This warning can be suppressed with '#[allow(lint(self_transfer))]' applied to the 'module' or module member ('const', 'fun', or 'struct')
The fix suggested by this particular lint is to return the Item from the function instead of transferring it, and let the caller decide what to do with the object.
Default and Extra Lints
Lints come in two tiers. The default tier contains the most important Sui-specific checks, and runs on every compilation - a plain sui move build or sui move test reports these warnings too. The extra tier adds two more Sui checks and a set of code style lints; it runs when linting is explicitly requested - by sui move lint or the --lint flag.
Suppressing Lints
Lints are heuristics, and sometimes the flagged code is intentional. A lint can be suppressed with the #[allow(lint(<name>))] attribute, applied to a module or a module member, using the lint name printed in the warning:
public struct Account has key { id: UID }
/// An account object, deliberately created for and owned by the sender.
#[allow(lint(self_transfer))]
public fun new_account(ctx: &mut TxContext) {
transfer::transfer(
Account { id: object::new(ctx) },
ctx.sender(),
);
}
A single attribute can suppress multiple lints: #[allow(lint(share_owned, self_transfer))]. Treat suppressions like any other exception - keep them narrow (prefer a function over the whole module) and explain the reason in a comment or doc comment.
Lints in CI
To enforce a warning-free codebase, add the --warnings-are-errors flag - the command then fails with a non-zero exit code on any warning, including lints:
sui move lint --test --warnings-are-errors
For tooling that consumes the output programmatically, --json-errors switches diagnostics to JSON format.
Lint Reference
The linter groups its checks into two sets: the default lints that run on every compilation, and the extra lints that only run under the --lint flag.
Default Lints
These run on every compilation:
| Lint | Code | What it flags |
|---|---|---|
| share_owned | W99000 | Sharing an object that may have been previously owned; share objects in the transaction that creates them |
| self_transfer | W99001 | Transferring a new object to the sender instead of returning it; hurts composability |
| custom_state_change | W99002 | A custom transfer/share/freeze policy on a type with store; the public_* storage functions can bypass it |
| coin_field | W99003 | A struct field of type Coin<T>; Balance<T> is cheaper and usually the right choice |
| freeze_wrapped | W99004 | Freezing an object that wraps other objects |
| collection_equality | W99005 | Comparing dynamic collections with ==; only the id and size are compared, never the contents |
| public_random | W99006 | A public function taking Random; exposes randomness to composition attacks |
| missing_key | W99007 | A struct with an id: UID field but no key ability |
| public_entry | W99010 | Unnecessary entry modifier on a public function |
| uncallable_function | W99011 | A function that can never be called in a transaction, such as an entry function taking &mut Clock |
Extra Lints
Enabled by sui move lint or the --lint flag:
| Lint | Code | What it flags |
|---|---|---|
| freezing_capability | W99008 | Freezing a type that looks like a capability |
| prefer_mut_tx_context | W99009 | A public function taking &TxContext; prefer &mut TxContext to keep the signature future-proof |
The extra tier also includes code style lints (codes W04xxx): constant_naming, while_true, unnecessary_math, unneeded_return, abort_without_constant, loop_without_exit, unnecessary_conditional, self_assignment, redundant_ref_deref, unnecessary_unit, always_equal_operands, and combinable_comparisons. Each flags a small readability or correctness issue and suggests the simpler equivalent.
Summary
| Command | Description |
|---|---|
| sui move lint | Compile the package and run the full lint set |
| sui move lint --test | Also lint the code in the tests directory |
| sui move lint --warnings-are-errors | Fail on any warning - for CI |
| sui move build / sui move test | Run the default lint tier |
| sui move test --lint | Run tests with the full lint set |
| --no-lint | Disable linters entirely |
Further Reading
- Code Quality Checklist - a broader review checklist that lints automate a part of.
- Move CLI reference in the Sui Documentation.