Skip to content

Commit d067e78

Browse files
feat(move): Move view functions (compiler implementation) - Linter Warning (#11444)
# Description of change Linter warning implementation This change gates the Iota view-function suggestion lint behind `--lint`. `ViewFunctionVisitor` is no longer included in the default Iota linter set, so normal builds do not emit warnings for functions that could be annotated with` #[view]`. The visitor now only runs at `LintLevel::All`, which is enabled through the existing `--lint` flag. ## Links to any relevant issues fixes #11413 --------- Co-authored-by: BigB <51323441+bingyanglin@users.noreply.github.com>
1 parent 72c0c06 commit d067e78

13 files changed

Lines changed: 936 additions & 1 deletion

File tree

external-crates/move/crates/move-compiler/src/iota_mode/linters/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub mod public_mut_tx_context;
2525
pub mod public_random;
2626
pub mod self_transfer;
2727
pub mod share_owned;
28+
pub mod view_function;
2829

2930
pub const TRANSFER_MOD_NAME: &str = "transfer";
3031
pub const TRANSFER_FUN: &str = "transfer";
@@ -73,6 +74,7 @@ pub const PUBLIC_RANDOM_FILTER_NAME: &str = "public_random";
7374
pub const MISSING_KEY_FILTER_NAME: &str = "missing_key";
7475
pub const FREEZING_CAPABILITY_FILTER_NAME: &str = "freezing_capability";
7576
pub const PREFER_MUTABLE_TX_CONTEXT_FILTER_NAME: &str = "prefer_mut_tx_context";
77+
pub const VIEW_FUNCTION_FILTER_NAME: &str = "view_function";
7678

7779
pub const RANDOM_MOD_NAME: &str = "random";
7880
pub const RANDOM_STRUCT_NAME: &str = "Random";
@@ -92,6 +94,7 @@ pub enum LinterDiagnosticCode {
9294
MissingKey,
9395
FreezingCapability,
9496
PreferMutableTxContext,
97+
ViewFunction,
9598
}
9699

97100
pub fn known_filters() -> (Option<Symbol>, Vec<WarningFilter>) {
@@ -157,6 +160,12 @@ pub fn known_filters() -> (Option<Symbol>, Vec<WarningFilter>) {
157160
LinterDiagnosticCode::PreferMutableTxContext as u8,
158161
Some(PREFER_MUTABLE_TX_CONTEXT_FILTER_NAME),
159162
),
163+
WarningFilter::code(
164+
Some(LINT_WARNING_PREFIX),
165+
LinterDiagnosticCategory::Iota as u8,
166+
LinterDiagnosticCode::ViewFunction as u8,
167+
Some(VIEW_FUNCTION_FILTER_NAME),
168+
),
160169
];
161170

162171
(Some(ALLOW_ATTR_CATEGORY.into()), filters)
@@ -178,6 +187,7 @@ pub fn linter_visitors(level: LintLevel) -> Vec<Visitor> {
178187
LintLevel::All => {
179188
let mut visitors = linter_visitors(LintLevel::Default);
180189
visitors.extend([
190+
view_function::ViewFunctionVisitor.visitor(),
181191
freezing_capability::WarnFreezeCapability.visitor(),
182192
public_mut_tx_context::PreferMutableTxContext.visitor(),
183193
]);
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) 2026 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Warns when a function satisfies view constraints but is not annotated with
5+
//! `#[view]`.
6+
7+
use super::{LINT_WARNING_PREFIX, LinterDiagnosticCategory, LinterDiagnosticCode};
8+
use crate::{
9+
diag,
10+
diagnostics::codes::{DiagnosticInfo, Severity, custom},
11+
expansion::ast::ModuleIdent,
12+
iota_mode::{known_attributes::view::ViewAttribute, typing::is_valid_view_signature},
13+
parser::ast::FunctionName,
14+
shared::Identifier,
15+
typing::{ast as T, visitor::simple_visitor},
16+
};
17+
18+
const VIEW_FUNCTION_DIAG: DiagnosticInfo = custom(
19+
LINT_WARNING_PREFIX,
20+
Severity::Warning,
21+
LinterDiagnosticCategory::Iota as u8,
22+
LinterDiagnosticCode::ViewFunction as u8,
23+
"function can be marked '#[view]'",
24+
);
25+
26+
simple_visitor!(
27+
ViewFunctionVisitor,
28+
fn visit_function_custom(
29+
&mut self,
30+
_module: ModuleIdent,
31+
fname: FunctionName,
32+
fdef: &T::Function,
33+
) -> bool {
34+
if fdef.attributes.is_test_or_test_only() {
35+
return false;
36+
}
37+
38+
if fdef.attributes.get_(&ViewAttribute.into()).is_some() {
39+
return false;
40+
}
41+
42+
if !is_valid_view_signature(&fdef.visibility, &fdef.signature) {
43+
return false;
44+
}
45+
46+
let msg = format!(
47+
"Function '{}' satisfies view constraints and can be annotated with '#[view]'",
48+
fname
49+
);
50+
let mut d = diag!(VIEW_FUNCTION_DIAG, (fname.loc(), msg));
51+
d.add_note("Add '#[view]' to make the function explicitly callable as a view function.");
52+
self.add_diag(d);
53+
54+
true
55+
}
56+
);

external-crates/move/crates/move-compiler/src/iota_mode/typing.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,23 @@ fn view_signature(
622622
view_return_ty(context, view_loc, name, return_type);
623623
}
624624

625+
pub(crate) fn is_valid_view_signature(
626+
visibility: &Visibility,
627+
signature: &FunctionSignature,
628+
) -> bool {
629+
let FunctionSignature {
630+
type_parameters: _,
631+
parameters,
632+
return_type,
633+
} = signature;
634+
635+
is_valid_view_visibility(visibility)
636+
&& is_valid_view_return_ty(return_type)
637+
&& parameters
638+
.iter()
639+
.all(|(mutability, _, param_ty)| is_valid_view_param_ty(mutability, param_ty))
640+
}
641+
625642
fn is_valid_view_visibility(visibility: &Visibility) -> bool {
626643
matches!(visibility, Visibility::Public(_))
627644
}
@@ -683,6 +700,12 @@ fn view_return_ty(context: &mut Context, view_loc: Loc, name: FunctionName, retu
683700
}
684701
}
685702

703+
fn is_valid_view_return_ty(return_type: &Type) -> bool {
704+
!matches!(return_type.value, Type_::Unit)
705+
&& !contains_reference_ty(return_type)
706+
&& !contains_view_unsafe_by_value_ty(return_type)
707+
}
708+
686709
/// A valid view param type is
687710
/// - A primitive (including strings and non-object structs)
688711
/// - A vector of primitives (including nested vectors)
@@ -742,6 +765,12 @@ fn view_param_ty(
742765
}
743766
}
744767

768+
fn is_valid_view_param_ty(mutability: &Mutability, param_ty: &Type) -> bool {
769+
!matches!(mutability, Mutability::Mut(_))
770+
&& !contains_mutable_reference_ty(param_ty)
771+
&& !contains_view_unsafe_by_value_ty(param_ty)
772+
}
773+
745774
fn contains_reference_ty(ty: &Type) -> bool {
746775
contains_reference_ty_where(ty, |_| true)
747776
}

external-crates/move/crates/move-compiler/tests/iota_mode/linter/collection_equality.snap

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ info:
55
edition: legacy
66
lint: true
77
---
8+
warning[Lint W99010]: function can be marked '#[view]'
9+
┌─ tests/iota_mode/linter/collection_equality.move:17:16
10+
11+
17 │ public fun bag_eq(bag1: &Bag, bag2: &Bag): bool {
12+
^^^^^^ Function 'bag_eq' satisfies view constraints and can be annotated with '#[view]'
13+
14+
= Add '#[view]' to make the function explicitly callable as a view function.
15+
= This warning can be suppressed with '#[allow(lint(view_function))]' applied to the 'module' or module member ('const', 'fun', or 'struct')
16+
817
warning[Lint W99005]: possibly useless collections compare
918
┌─ tests/iota_mode/linter/collection_equality.move:18:14
1019
@@ -14,6 +23,15 @@ warning[Lint W99005]: possibly useless collections compare
1423
= Equality for collections of type 'iota::bag::Bag' IS NOT a structural check based on content
1524
= This warning can be suppressed with '#[allow(lint(collection_equality))]' applied to the 'module' or module member ('const', 'fun', or 'struct')
1625

26+
warning[Lint W99010]: function can be marked '#[view]'
27+
┌─ tests/iota_mode/linter/collection_equality.move:21:16
28+
29+
21 │ public fun obj_bag_neq(bag1: &ObjectBag, bag2: &ObjectBag): bool {
30+
^^^^^^^^^^^ Function 'obj_bag_neq' satisfies view constraints and can be annotated with '#[view]'
31+
32+
= Add '#[view]' to make the function explicitly callable as a view function.
33+
= This warning can be suppressed with '#[allow(lint(view_function))]' applied to the 'module' or module member ('const', 'fun', or 'struct')
34+
1735
warning[Lint W99005]: possibly useless collections compare
1836
┌─ tests/iota_mode/linter/collection_equality.move:22:14
1937
@@ -23,6 +41,15 @@ warning[Lint W99005]: possibly useless collections compare
2341
= Equality for collections of type 'iota::object_bag::ObjectBag' IS NOT a structural check based on content
2442
= This warning can be suppressed with '#[allow(lint(collection_equality))]' applied to the 'module' or module member ('const', 'fun', or 'struct')
2543

44+
warning[Lint W99010]: function can be marked '#[view]'
45+
┌─ tests/iota_mode/linter/collection_equality.move:25:16
46+
47+
25 │ public fun table_eq(table1: &Table<u64, u64>, table2: &Table<u64, u64>): bool {
48+
^^^^^^^^ Function 'table_eq' satisfies view constraints and can be annotated with '#[view]'
49+
50+
= Add '#[view]' to make the function explicitly callable as a view function.
51+
= This warning can be suppressed with '#[allow(lint(view_function))]' applied to the 'module' or module member ('const', 'fun', or 'struct')
52+
2653
warning[Lint W99005]: possibly useless collections compare
2754
┌─ tests/iota_mode/linter/collection_equality.move:26:16
2855
@@ -32,6 +59,15 @@ warning[Lint W99005]: possibly useless collections compare
3259
= Equality for collections of type 'iota::table::Table' IS NOT a structural check based on content
3360
= This warning can be suppressed with '#[allow(lint(collection_equality))]' applied to the 'module' or module member ('const', 'fun', or 'struct')
3461

62+
warning[Lint W99010]: function can be marked '#[view]'
63+
┌─ tests/iota_mode/linter/collection_equality.move:29:16
64+
65+
29 │ public fun obj_table_eq<K: copy + drop + store, V: key + store>(
66+
│ ^^^^^^^^^^^^ Function 'obj_table_eq' satisfies view constraints and can be annotated with '#[view]'
67+
68+
= Add '#[view]' to make the function explicitly callable as a view function.
69+
= This warning can be suppressed with '#[allow(lint(view_function))]' applied to the 'module' or module member ('const', 'fun', or 'struct')
70+
3571
warning[Lint W99005]: possibly useless collections compare
3672
┌─ tests/iota_mode/linter/collection_equality.move:33:20
3773
@@ -41,6 +77,15 @@ warning[Lint W99005]: possibly useless collections compare
4177
= Equality for collections of type 'iota::object_table::ObjectTable' IS NOT a structural check based on content
4278
= This warning can be suppressed with '#[allow(lint(collection_equality))]' applied to the 'module' or module member ('const', 'fun', or 'struct')
4379

80+
warning[Lint W99010]: function can be marked '#[view]'
81+
┌─ tests/iota_mode/linter/collection_equality.move:36:16
82+
83+
36 │ public fun linked_table_neq(table1: &LinkedTable<u64, u64>, table2: &LinkedTable<u64, u64>): bool {
84+
^^^^^^^^^^^^^^^^ Function 'linked_table_neq' satisfies view constraints and can be annotated with '#[view]'
85+
86+
= Add '#[view]' to make the function explicitly callable as a view function.
87+
= This warning can be suppressed with '#[allow(lint(view_function))]' applied to the 'module' or module member ('const', 'fun', or 'struct')
88+
4489
warning[Lint W99005]: possibly useless collections compare
4590
┌─ tests/iota_mode/linter/collection_equality.move:37:16
4691
@@ -50,6 +95,15 @@ warning[Lint W99005]: possibly useless collections compare
5095
= Equality for collections of type 'iota::linked_table::LinkedTable' IS NOT a structural check based on content
5196
= This warning can be suppressed with '#[allow(lint(collection_equality))]' applied to the 'module' or module member ('const', 'fun', or 'struct')
5297

98+
warning[Lint W99010]: function can be marked '#[view]'
99+
┌─ tests/iota_mode/linter/collection_equality.move:40:16
100+
101+
40 │ public fun table_vec_eq(table1: &TableVec<u64>, table2: &TableVec<u64>): bool {
102+
^^^^^^^^^^^^ Function 'table_vec_eq' satisfies view constraints and can be annotated with '#[view]'
103+
104+
= Add '#[view]' to make the function explicitly callable as a view function.
105+
= This warning can be suppressed with '#[allow(lint(view_function))]' applied to the 'module' or module member ('const', 'fun', or 'struct')
106+
53107
warning[Lint W99005]: possibly useless collections compare
54108
┌─ tests/iota_mode/linter/collection_equality.move:41:16
55109
@@ -59,6 +113,15 @@ warning[Lint W99005]: possibly useless collections compare
59113
= Equality for collections of type 'iota::table_vec::TableVec' IS NOT a structural check based on content
60114
= This warning can be suppressed with '#[allow(lint(collection_equality))]' applied to the 'module' or module member ('const', 'fun', or 'struct')
61115

116+
warning[Lint W99010]: function can be marked '#[view]'
117+
┌─ tests/iota_mode/linter/collection_equality.move:44:16
118+
119+
44 │ public fun vec_map_eq(vec1: &VecMap<u64, u64>, vec2: &VecMap<u64, u64>): bool {
120+
^^^^^^^^^^ Function 'vec_map_eq' satisfies view constraints and can be annotated with '#[view]'
121+
122+
= Add '#[view]' to make the function explicitly callable as a view function.
123+
= This warning can be suppressed with '#[allow(lint(view_function))]' applied to the 'module' or module member ('const', 'fun', or 'struct')
124+
62125
warning[Lint W99005]: possibly useless collections compare
63126
┌─ tests/iota_mode/linter/collection_equality.move:45:14
64127
@@ -68,6 +131,15 @@ warning[Lint W99005]: possibly useless collections compare
68131
= Equality for collections of type 'iota::vec_map::VecMap' IS NOT a structural check based on content
69132
= This warning can be suppressed with '#[allow(lint(collection_equality))]' applied to the 'module' or module member ('const', 'fun', or 'struct')
70133

134+
warning[Lint W99010]: function can be marked '#[view]'
135+
┌─ tests/iota_mode/linter/collection_equality.move:48:16
136+
137+
48 │ public fun vec_set_eq(vec1: &VecSet<u64>, vec2: &VecSet<u64>): bool {
138+
^^^^^^^^^^ Function 'vec_set_eq' satisfies view constraints and can be annotated with '#[view]'
139+
140+
= Add '#[view]' to make the function explicitly callable as a view function.
141+
= This warning can be suppressed with '#[allow(lint(view_function))]' applied to the 'module' or module member ('const', 'fun', or 'struct')
142+
71143
warning[Lint W99005]: possibly useless collections compare
72144
┌─ tests/iota_mode/linter/collection_equality.move:49:14
73145

external-crates/move/crates/move-compiler/tests/iota_mode/linter/custom_state_change.snap

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,12 @@ warning[Lint W99002]: potentially unenforceable custom transfer/share/freeze pol
4343
4444
= A custom freeze policy for a given type is implemented through calling the private 'freeze_object' function variant in the module defining this type
4545
= This warning can be suppressed with '#[allow(lint(custom_state_change))]' applied to the 'module' or module member ('const', 'fun', or 'struct')
46+
47+
warning[Lint W99010]: function can be marked '#[view]'
48+
┌─ tests/iota_mode/linter/custom_state_change.move:42:16
49+
50+
42 │ public fun sender(_: &TxContext): address {
51+
^^^^^^ Function 'sender' satisfies view constraints and can be annotated with '#[view]'
52+
53+
= Add '#[view]' to make the function explicitly callable as a view function.
54+
= This warning can be suppressed with '#[allow(lint(view_function))]' applied to the 'module' or module member ('const', 'fun', or 'struct')

external-crates/move/crates/move-compiler/tests/iota_mode/linter/false_positive_share_owned.snap

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,12 @@ warning[Lint W99000]: possible owned object share
3838
│ Potential abort from a (potentially) owned object created by a different transaction.
3939
4040
= This warning can be suppressed with '#[allow(lint(share_owned))]' applied to the 'module' or module member ('const', 'fun', or 'struct')
41+
42+
warning[Lint W99010]: function can be marked '#[view]'
43+
┌─ tests/iota_mode/linter/false_positive_share_owned.move:48:16
44+
45+
48 │ public fun sender(_: &TxContext): address {
46+
^^^^^^ Function 'sender' satisfies view constraints and can be annotated with '#[view]'
47+
48+
= Add '#[view]' to make the function explicitly callable as a view function.
49+
= This warning can be suppressed with '#[allow(lint(view_function))]' applied to the 'module' or module member ('const', 'fun', or 'struct')

external-crates/move/crates/move-compiler/tests/iota_mode/linter/self_transfer.snap

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,12 @@ warning[Lint W99001]: non-composable transfer to sender
4343
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Transfer of an object to transaction sender address
4444
4545
= This warning can be suppressed with '#[allow(lint(self_transfer))]' applied to the 'module' or module member ('const', 'fun', or 'struct')
46+
47+
warning[Lint W99010]: function can be marked '#[view]'
48+
┌─ tests/iota_mode/linter/self_transfer.move:72:16
49+
50+
72 │ public fun sender(_: &TxContext): address {
51+
^^^^^^ Function 'sender' satisfies view constraints and can be annotated with '#[view]'
52+
53+
= Add '#[view]' to make the function explicitly callable as a view function.
54+
= This warning can be suppressed with '#[allow(lint(view_function))]' applied to the 'module' or module member ('const', 'fun', or 'struct')

external-crates/move/crates/move-compiler/tests/iota_mode/linter/true_negative_share_owned.snap

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,11 @@ info:
55
edition: legacy
66
lint: true
77
---
8-
8+
warning[Lint W99010]: function can be marked '#[view]'
9+
┌─ tests/iota_mode/linter/true_negative_share_owned.move:79:16
10+
11+
79 │ public fun sender(_: &TxContext): address {
12+
^^^^^^ Function 'sender' satisfies view constraints and can be annotated with '#[view]'
13+
14+
= Add '#[view]' to make the function explicitly callable as a view function.
15+
= This warning can be suppressed with '#[allow(lint(view_function))]' applied to the 'module' or module member ('const', 'fun', or 'struct')

external-crates/move/crates/move-compiler/tests/iota_mode/linter/true_positive_share_owned.snap

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,12 @@ warning[Lint W99000]: possible owned object share
3838
│ Potential abort from a (potentially) owned object created by a different transaction.
3939
4040
= This warning can be suppressed with '#[allow(lint(share_owned))]' applied to the 'module' or module member ('const', 'fun', or 'struct')
41+
42+
warning[Lint W99010]: function can be marked '#[view]'
43+
┌─ tests/iota_mode/linter/true_positive_share_owned.move:48:16
44+
45+
48 │ public fun sender(_: &TxContext): address {
46+
^^^^^^ Function 'sender' satisfies view constraints and can be annotated with '#[view]'
47+
48+
= Add '#[view]' to make the function explicitly callable as a view function.
49+
= This warning can be suppressed with '#[allow(lint(view_function))]' applied to the 'module' or module member ('const', 'fun', or 'struct')

0 commit comments

Comments
 (0)