llms.txt
Skip to main content

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:

LintCodeWhat it flags
share_ownedW99000Sharing an object that may have been previously owned; share objects in the transaction that creates them
self_transferW99001Transferring a new object to the sender instead of returning it; hurts composability
custom_state_changeW99002A custom transfer/share/freeze policy on a type with store; the public_* storage functions can bypass it
coin_fieldW99003A struct field of type Coin<T>; Balance<T> is cheaper and usually the right choice
freeze_wrappedW99004Freezing an object that wraps other objects
collection_equalityW99005Comparing dynamic collections with ==; only the id and size are compared, never the contents
public_randomW99006A public function taking Random; exposes randomness to composition attacks
missing_keyW99007A struct with an id: UID field but no key ability
public_entryW99010Unnecessary entry modifier on a public function
uncallable_functionW99011A 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:

LintCodeWhat it flags
freezing_capabilityW99008Freezing a type that looks like a capability
prefer_mut_tx_contextW99009A 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

CommandDescription
sui move lintCompile the package and run the full lint set
sui move lint --testAlso lint the code in the tests directory
sui move lint --warnings-are-errorsFail on any warning - for CI
sui move build / sui move testRun the default lint tier
sui move test --lintRun tests with the full lint set
--no-lintDisable linters entirely

Further Reading

llms.txt