Sui Linters and Warnings Update Increases Coder Velocity

New developer experience features, including linters and compiler warnings, give builders useful warnings while coding.

Sui Linters and Warnings Update Increases Coder Velocity

New linter support and enhanced warning messages launched on Sui improve the Move developer experience, offering coding support common in many programming ecosystems. Six new linters, mostly dealing with object handling, find potentially problematic patterns in Sui specific code. In addition, the Move compiler now includes warnings about unused constructs, which should prove helpful to Move developers. 

These additions help both new and experienced developers avoid problems with their code as they build apps taking advantage of Sui's innovative technology. Both the linters and warning messages include suppression options, letting developers customize their workflows.

Linting support

Many developers depend on linters to catch problems in their code as they work, especially in high productivity environments where code reviews might be rare. However, because Move is relatively new, it hasn't enjoyed the support of coding aids common in languages that have been around for years.

With this new framework on Sui, linting support is currently opt-in. Developers need to specify the —-lint flag to any build, test, publish, and upgrade commands to see linter messages. In a future release, this framework will switch to an opt-out mode (—-no-lint) so that linter messages display by default.

Sui currently supports six different linters, described below, with more planned. Community feedback will guide future linter development.

1. Coin field linter

This analysis flags uses of the sui::coin::Coin object in fields of other objects and structs. In most cases, developers should use sui::balance::Balance to save space.

As an example, consider this simple module defining an object containing a field of the sui::coin::Coin type:

#[allow(unused_field)]
module coin_field::test {
    struct S1 {}

    struct S2 has key, store {
        id: sui::object::UID,
        c: sui::coin::Coin<S1>,
    }
}

Building this module results in the following linter message:

warning[Lint W03001]: sub-optimal 'sui::coin::Coin' field type
  ┌─ ./sources/test.move:5:12
  │
5 │     struct S2 has key, store {
  │            ^^ The field 'c' of 'S2' has type 'sui::coin::Coin'
6 │         id: sui::object::UID,
7 │         c: sui::coin::Coin<S1>,
  │         - Storing 'sui::balance::Balance' in this field will typically be more space-efficient
  │
  = This warning can be suppressed with '#[lint_allow(coin_field)]' applied to the 'module' or module member ('const', 'fun', or 'struct')

Note that the #[allow(unused_field)]annotation in the source code suppresses printing unused field warnings to make the output more succinct. The Enhanced warnings section of this article, below, goes into more detail about new warnings as well as warning and linter message suppression. 

2. Collection equality linter

This linter flags situations when instances such as sui::table::Table, sui::table_vec::TableVec, sui::bag::Bag are being compared for (in)equality. The reasoning behind this linter is that this type of comparison is not very useful and does not take into consideration structural (in)equality.

As an example, consider this basic module containing a function attempting to compare references to two different instances of sui::bag::Bag:

module collection_eq::test {
    public fun bag_eq(bag1: &sui::bag::Bag, bag2: &sui::bag::Bag): bool {
        bag1 == bag2
    }
}

Building this module results in the following linter message:

warning[Lint W05001]: possibly useless collections compare
  ┌─ ./sources/test.move:3:14
  │
3 │         bag1 == bag2
  │              ^^ Comparing collections of type 'sui::bag::Bag' may yield unexpected result.
  │
  = Equality for collections of type 'sui::bag::Bag' IS NOT a structural check based on content
  = This warning can be suppressed with '#[lint_allow(collection_equality)]' applied to the 'module' or module member ('const', 'fun', or 'struct')

3. Custom state change linter

This linter flags potential custom implementations of transfer, share, and freeze calls on objects that already have a store ability and where the developer can use public variants of these calls. Using these calls can be dangerous as custom transfer, share, and freeze operations become unenforceable in this situation.  A function is considered a potential custom implementation if it takes as a parameter an instance of a struct type defined in a given module with a store ability and passes it as an argument to a private transfer, share, or freeze call.

As an example, consider this simple module containing a function attempting to use the public sui::transfer::transfer function to transfer an object with the store ability passed as an argument:

#[allow(unused_field)]
module custom_state_change::test {
    struct S1 has key, store {
        id: sui::object::UID
    }

    public fun custom_transfer(o: S1, a: address) {
        sui::transfer::transfer(o, a)
    }
}

Building this module results in the following linter message:

warning[Lint W02001]: potentially unenforceable custom transfer/share/freeze policy
  ┌─ ./sources/test.move:7:16
  │
7 │     public fun custom_transfer(o: S1, a: address) {
  │                ^^^^^^^^^^^^^^^ - An instance of a module-private type with a store ability to be transferred coming from here
  │                │                
  │                Potential unintended implementation of a custom transfer function.
8 │         sui::transfer::transfer(o, a)
  │                        -------- Instances of a type with a store ability can be transferred using the public_transfer function which often negates the intent of enforcing a custom transfer policy
  │
  = A custom transfer policy for a given type is implemented through calling the private transfer function variant in the module defining this type
  = This warning can be suppressed with '#[lint_allow(custom_state_change)]' applied to the 'module' or module member ('const', 'fun', or 'struct')

4. Freeze wrapped linter

This linter flags freezing objects containing (transitively or not) inner objects. In other words, it flags freezing of objects whose fields (directly or not) wrap other objects. Freezing such objects prevents unwrapping of inner objects.

As an example, consider this basic module containing a function attempting to freeze an object of type Wrapper containing a field of another object type Inner:

#[allow(unused_field)]
module freeze_wrapped::test {
    struct Inner has key, store {
        id: sui::object::UID
    }

    struct Wrapper has key, store {
        id: sui::object::UID,
        inner: Inner,
    }

    public fun freeze(w: Wrapper) {
        sui::transfer::public_freeze_object(w);
    }
}

Building this module results in the following linter message:

warning[Lint W04001]: attempting to freeze wrapped objects
   ┌─ ./sources/test.move:13:45
   │
 9 │         inner: Inner,
   │                ----- The field of this type is a wrapped object
   ·
13 │         sui::transfer::public_freeze_object(w);
   │                                             ^ Freezing an object of type 'Wrapper' also freezes all objects wrapped in its field 'inner'.
   │
   = This warning can be suppressed with '#[lint_allow(freeze_wrapped)]' applied to the 'module' or module member ('const', 'fun', or 'struct')

5. Self transfer linter

This linter flags transfers of an object to the transaction sender obtained from the sui::tx_context::sender() call. The goal of this linter is to encourage developers to return objects from functions rather than transferring them to the transaction sender. Returning objects from functions increases composability of functions in Programmable Transaction Blocks by allowing callers to directly use the returned object.

As an example, consider this simple module containing a function attempting to transfer a freshly created object to the transaction sender:

module self_transfer::test {
    struct S1 has key, store {
        id: sui::object::UID
    }

    public fun public_transfer(ctx: &mut sui::tx_context::TxContext) {
        let o = S1 { id: sui::object::new(ctx) };
        sui::transfer::public_transfer(o, sui::tx_context::sender(ctx))
    }
}

Building this module results in the following linter message:

warning[Lint W01001]: non-composable transfer to sender
  ┌─ ./sources/test.move:8:9
  │
6 │     public fun public_transfer(ctx: &mut sui::tx_context::TxContext) {
  │                --------------- Returning an object from a function, allows a caller to use the object and enables composability via programmable transactions.
7 │         let o = S1 { id: sui::object::new(ctx) };
8 │         sui::transfer::public_transfer(o, sui::tx_context::sender(ctx))
  │         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  │         │                                 │
  │         │                                 Transaction sender address coming from here
  │         Transfer of an object to transaction sender address in function public_transfer
  │
  = This warning can be suppressed with '#[lint_allow(self_transfer)]' applied to the 'module' or module member ('const', 'fun', or 'struct')

6. Share owned linter

This linter flags making objects passed as function parameters or resulting from unpacking shareable (which are likely already owned), which would lead to an abort. A suggested pattern is to create a fresh object and share it within the same function. Typically, any object passed to a function by value is an owned object.

As an example, consider this basic module containing a function attempting to share an object passed as an argument (data flow for this object is tracked in the function):

#[allow(unused_field)]
module unused::test {
    struct Obj has key, store {
        id: sui::object::UID
    }

    public fun arg_object(o: Obj) {
        let arg = o;
        sui::transfer::public_share_object(arg);
    }
}

Building this module results in the following linter message:

warning[Lint W00001]: possible owned object share
  ┌─ ./sources/test.move:9:9
  │
7 │     public fun arg_object(o: Obj) {
  │                           - A potentially owned object coming from here
8 │         let arg = o;
9 │         sui::transfer::public_share_object(arg);
  │         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  │         │                                  │
  │         │                                  Creating a fresh object and sharing it within the same function will ensure this does not abort.
  │         Potential abort from a (potentially) owned object created by a different transaction.
  │
  = This warning can be suppressed with '#[lint_allow(share_owned)]' applied to the 'module' or module member ('const', 'fun', or 'struct')

Enhanced warnings

Along with linters and their alerts, the Move compiler now helps developers by warning about unused constructs, including unused functions, constants, (function) type parameters, and struct/object fields. Although many developers find these warnings helpful, they might be unwelcome, particularly for code that compiled previously without any warnings. Developers who want to suppress these warnings for a specific code element can specify an annotation (of the form #[...]) for a module, a constant, a type definition, or a function.

Warning suppression example

The following code provides an example that generates the new warnings. This basic module defines an unused constant,:

module unused::test {
    const UNUSED_CONST: u64 = 42;
}

Building this module results in the following warning:

warning[W09011]: unused constant
  ┌─ ./sources/test.move:2:11
  │
2 │     const UNUSED_CONST: u64 = 42;
  │           ^^^^^^^^^^^^ The constant 'UNUSED_CONST' is never used. Consider removing it.
  │
  = This warning can be suppressed with '#[allow(unused_const)]' applied to the 'module' or module member ('const', 'fun', or 'struct')

The compiler returns a suggestion about how to suppress a specific warning. In this case, place the #[allow(unused_const)] annotation at the constant level as follows:

module unused::test {
    #[allow(unused_const)]
    const UNUSED_CONST: u64 = 42;
}

Including the annotation at the module level suppresses multiple warnings of the same kind:

#[allow(unused_const)]
module unused::test {
    const UNUSED_CONST: u64 = 42;
}

Linter suppression

Developers can suppress linter messages in a similar fashion to standard compiler warnings. A linter message includes a description of an annotation that developers can use to suppress it. The keyword used to suppress a linter warning in the annotation is lint_allow instead of allow, the latter used for suppressing standard warnings. If a developer chooses linter suppression, the compiler also prints simple statistics on how many messages and of how many kinds it suppressed.

Developing on Sui

The Move programming language arrived on the scene just four years ago. Although the safety and utility of the language gave it immediate popularity in the builder community, the quantity of developer tools lags behind more mature languages. Adding linters and compiler warnings, some tailored specifically to Move on Sui, is just one effort among many to improve the developer experience.

However, the open source nature of Move means the entire builder community can and should play a part in improving the developer experience. How people contribute ranges from expressing a need in a forum or other venue to submitting a pull request in the repo with a new linter implementation proposal or other language improvement. Whichever way the community wants to contribute, the first step towards helping to develop Move involves engaging in the Sui forums.