-
Notifications
You must be signed in to change notification settings - Fork 447
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Linter: Warn when number primitive is annotated with #[ink(topic)]
#1837
Merged
Merged
Changes from 10 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
7ca2cf7
The initial structure of the linting crate and tests
574fc18
Implement the lint that checks primitive types in expanded code
jubnzv 0938d13
The initial implementation of the `primitive_topic` lint
jubnzv 173d9ae
chore(all): Run clippy
jubnzv 4068abc
fix(fmt)
jubnzv 72bb431
fix(primitive_topic): Lint suppressions
jubnzv aa802c3
chore: cleanup
jubnzv 6157877
chore: make warning messages more consistent
jubnzv 941125a
Merge remote-tracking branch 'origin/master' into 1436-primitive-topic
jubnzv f10985b
feat(lint): support events 2.0
jubnzv a89088b
chore(all): Fix comments; remove needless empty file
jubnzv 3f99ce9
chore(all): Update `clippy_utils` version
jubnzv f66bd95
fix Cargo.toml
jubnzv fe26eaf
Merge remote-tracking branch 'origin/master' into 1436-primitive-topic
jubnzv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
// Copyright 2018-2023 Parity Technologies (UK) Ltd. | ||
jubnzv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
use clippy_utils::{ | ||
diagnostics::span_lint_and_then, | ||
is_lint_allowed, | ||
match_def_path, | ||
source::snippet_opt, | ||
}; | ||
use if_chain::if_chain; | ||
use rustc_errors::Applicability; | ||
use rustc_hir::{ | ||
self, | ||
def::{ | ||
DefKind, | ||
Res, | ||
}, | ||
def_id::DefId, | ||
Arm, | ||
AssocItemKind, | ||
ExprKind, | ||
Impl, | ||
ImplItemKind, | ||
ImplItemRef, | ||
Item, | ||
ItemKind, | ||
Node, | ||
PatKind, | ||
QPath, | ||
}; | ||
use rustc_lint::{ | ||
LateContext, | ||
LateLintPass, | ||
}; | ||
use rustc_middle::ty::{ | ||
self, | ||
Ty, | ||
TyKind, | ||
}; | ||
use rustc_session::{ | ||
declare_lint, | ||
declare_lint_pass, | ||
}; | ||
|
||
declare_lint! { | ||
/// **What it does:** Checks for ink! contracts that use | ||
/// the [`#[ink(topic)]`](https://use.ink/macros-attributes/topic) annotation with primitive | ||
/// number types. Topics are discrete events for which it makes sense to filter. Typical | ||
/// examples of fields that should be filtered are `AccountId`, `bool` or `enum` variants. | ||
/// | ||
/// **Why is this bad?** It typically doesn't make sense to annotate types like `u32` or `i32` | ||
/// as a topic, if those fields can take continuous values that could be anywhere between | ||
/// `::MIN` and `::MAX`. An example of a case where it doesn't make sense at all to have a | ||
/// topic on the storage field is something like `value: Balance` in the examle below. | ||
/// | ||
/// **Known problems:** Events 2.0 currently are not supported: | ||
jubnzv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// https://github.com/paritytech/ink/pull/1827. | ||
/// | ||
/// **Example:** | ||
/// | ||
/// ```rust | ||
/// // Good | ||
/// // Filtering transactions based on source and destination addresses. | ||
/// #[ink(event)] | ||
/// pub struct Transaction { | ||
/// #[ink(topic)] | ||
/// src: Option<AccountId>, | ||
/// #[ink(topic)] | ||
/// dst: Option<AccountId>, | ||
/// value: Balance, | ||
/// } | ||
/// ``` | ||
/// | ||
/// ```rust | ||
/// // Bad | ||
/// // It typically makes no sense to filter `Balance`, since its value may varies from `::MAX` | ||
/// // to `::MIN`. | ||
/// #[ink(event)] | ||
/// pub struct Transaction { | ||
/// #[ink(topic)] | ||
/// src: Option<AccountId>, | ||
/// #[ink(topic)] | ||
/// dst: Option<AccountId>, | ||
/// #[ink(topic)] | ||
/// value: Balance, | ||
/// } | ||
/// ``` | ||
pub PRIMITIVE_TOPIC, | ||
Warn, | ||
"`#[ink(topic)]` should not be used with a number primitive" | ||
} | ||
|
||
declare_lint_pass!(PrimitiveTopic => [PRIMITIVE_TOPIC]); | ||
|
||
/// Returns `true` if `item` is an implementation of `::ink::env::Event` for a storage | ||
/// struct. If that's the case, it returns the name of this struct. | ||
fn is_ink_event_impl<'tcx>(cx: &LateContext<'tcx>, item: &'tcx Item<'_>) -> bool { | ||
if let Some(trait_ref) = cx.tcx.impl_trait_ref(item.owner_id) { | ||
match_def_path(cx, trait_ref.0.def_id, &["ink_env", "event", "Event"]) | ||
jubnzv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} else { | ||
false | ||
} | ||
} | ||
|
||
/// Returns `true` if `impl_item` is the `topics` function | ||
fn is_topics_function(impl_item: &ImplItemRef) -> bool { | ||
impl_item.kind == AssocItemKind::Fn { has_self: true } | ||
&& impl_item.ident.name.as_str() == "topics" | ||
} | ||
|
||
/// Returns `true` if `ty` is a primitive type that should not be annotated with | ||
jubnzv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// `#[ink(topic)]` | ||
fn is_primitive_number_ty(ty: &Ty) -> bool { | ||
matches!(ty.kind(), ty::Int(_) | ty::Uint(_)) | ||
} | ||
|
||
/// Reports field of the event structure | ||
jubnzv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
fn report_field(cx: &LateContext, event_def_id: DefId, field_name: &str) { | ||
if_chain! { | ||
if let Some(Node::Item(event_node)) = cx.tcx.hir().get_if_local(event_def_id); | ||
if let ItemKind::Struct(ref struct_def, _) = event_node.kind; | ||
if let Some(field) = struct_def.fields().iter().find(|f|{ f.ident.as_str() == field_name }); | ||
if !is_lint_allowed(cx, PRIMITIVE_TOPIC, field.hir_id); | ||
then { | ||
span_lint_and_then( | ||
cx, | ||
PRIMITIVE_TOPIC, | ||
field.span, | ||
"using `#[ink(topic)]` for a field with a primitive number type", | ||
|diag| { | ||
let snippet = snippet_opt(cx, field.span).expect("snippet must exist"); | ||
diag.span_suggestion( | ||
field.span, | ||
"consider removing `#[ink(topic)]`".to_string(), | ||
snippet, | ||
Applicability::Unspecified, | ||
); | ||
}, | ||
) | ||
} | ||
} | ||
} | ||
|
||
/// Returns `DefId` of the event struct for which `Topics` is implemented | ||
fn get_event_def_id(topics_impl: &Impl) -> Option<DefId> { | ||
if_chain! { | ||
if let rustc_hir::TyKind::Path(qpath) = &topics_impl.self_ty.kind; | ||
if let QPath::Resolved(_, path) = qpath; | ||
if let Res::Def(DefKind::Struct, def_id) = path.res; | ||
then { Some(def_id) } | ||
else { None } | ||
} | ||
} | ||
|
||
impl<'tcx> LateLintPass<'tcx> for PrimitiveTopic { | ||
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) { | ||
if_chain! { | ||
if let ItemKind::Impl(topics_impl) = &item.kind; | ||
if is_ink_event_impl(cx, item); | ||
if let Some(event_def_id) = get_event_def_id(topics_impl); | ||
then { | ||
topics_impl.items.iter().for_each(|impl_item| { | ||
if_chain! { | ||
// We need to extract field patterns from the event sturct matched in the | ||
// `topics` function to access their inferred types. | ||
// Here is the simplified example of the expanded code: | ||
// ``` | ||
// fn topics(/* ... */) { | ||
// match self { | ||
// MyEvent { | ||
// field_1: __binding_0, | ||
// field_2: __binding_1, | ||
// /* ... */ | ||
// .. | ||
// } => { /* ... */ } | ||
// } | ||
// } | ||
// ``` | ||
if is_topics_function(impl_item); | ||
let impl_item = cx.tcx.hir().impl_item(impl_item.id); | ||
if let ImplItemKind::Fn(_, eid) = impl_item.kind; | ||
let body = cx.tcx.hir().body(eid).value; | ||
if let ExprKind::Block (block, _) = body.kind; | ||
if let Some(match_self) = block.expr; | ||
if let ExprKind::Match(_, [Arm { pat: arm_pat, .. }], _) = match_self.kind; | ||
if let PatKind::Struct(_, pat_fields, _) = &arm_pat.kind; | ||
then { | ||
pat_fields.iter().for_each(|pat_field| { | ||
cx.tcx | ||
.has_typeck_results(pat_field.hir_id.owner.def_id) | ||
.then(|| { | ||
if let TyKind::Ref(_, ty, _) = cx | ||
.tcx | ||
.typeck(pat_field.hir_id.owner.def_id) | ||
.pat_ty(pat_field.pat) | ||
.kind() | ||
{ | ||
if is_primitive_number_ty(ty) { | ||
report_field(cx, | ||
event_def_id, | ||
pat_field.ident.as_str()) | ||
} | ||
} | ||
}); | ||
}) | ||
} | ||
} | ||
}) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
pub type TyAlias1 = i32; | ||
pub type TyAlias2 = TyAlias1; | ||
|
||
#[ink::contract] | ||
pub mod primitive_topic { | ||
|
||
#[ink(event)] | ||
pub struct Transaction { | ||
// Bad | ||
#[ink(topic)] | ||
value_1: u8, | ||
// Bad | ||
#[ink(topic)] | ||
value_2: Balance, | ||
// Bad | ||
#[ink(topic)] | ||
value_3: crate::TyAlias1, | ||
// Bad | ||
#[ink(topic)] | ||
value_4: crate::TyAlias2, | ||
} | ||
|
||
#[ink(storage)] | ||
pub struct PrimitiveTopic {} | ||
|
||
impl PrimitiveTopic { | ||
#[ink(constructor)] | ||
pub fn new() -> Self { | ||
Self {} | ||
} | ||
#[ink(message)] | ||
pub fn do_nothing(&mut self) {} | ||
} | ||
} | ||
|
||
fn main() {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
error: using `#[ink(topic)]` for a field with a primitive number type | ||
--> $DIR/primitive_topic.rs:11:9 | ||
| | ||
LL | value_1: u8, | ||
| ^^^^^^^^^^^ help: consider removing `#[ink(topic)]`: `value_1: u8` | ||
| | ||
= note: `-D primitive-topic` implied by `-D warnings` | ||
|
||
error: using `#[ink(topic)]` for a field with a primitive number type | ||
--> $DIR/primitive_topic.rs:14:9 | ||
| | ||
LL | value_2: Balance, | ||
| ^^^^^^^^^^^^^^^^ help: consider removing `#[ink(topic)]`: `value_2: Balance` | ||
|
||
error: using `#[ink(topic)]` for a field with a primitive number type | ||
--> $DIR/primitive_topic.rs:17:9 | ||
| | ||
LL | value_3: crate::TyAlias1, | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: consider removing `#[ink(topic)]`: `value_3: crate::TyAlias1` | ||
|
||
error: using `#[ink(topic)]` for a field with a primitive number type | ||
--> $DIR/primitive_topic.rs:20:9 | ||
| | ||
LL | value_4: crate::TyAlias2, | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: consider removing `#[ink(topic)]`: `value_4: crate::TyAlias2` | ||
|
||
error: aborting due to 4 previous errors | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
#[ink::contract] | ||
pub mod primitive_topic { | ||
|
||
#[ink(event)] | ||
pub struct Transaction { | ||
#[ink(topic)] | ||
src: Option<AccountId>, | ||
#[ink(topic)] | ||
dst: Option<AccountId>, | ||
// Good: no topic annotation | ||
value_1: Balance, | ||
// Good: suppressed warning | ||
#[ink(topic)] | ||
#[cfg_attr(dylint_lib = "ink_linting", allow(primitive_topic))] | ||
value_2: Balance, | ||
} | ||
|
||
#[ink(storage)] | ||
pub struct PrimitiveTopic {} | ||
|
||
impl PrimitiveTopic { | ||
#[ink(constructor)] | ||
pub fn new() -> Self { | ||
Self {} | ||
} | ||
#[ink(message)] | ||
pub fn do_nothing(&mut self) {} | ||
} | ||
} | ||
|
||
fn main() {} |
jubnzv marked this conversation as resolved.
Show resolved
Hide resolved
|
Empty file.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please update ref of
clippy_utils
to1d334696587ac22b3a9e651e7ac684ac9e0697b2
and bumpdylint_linting
&dylint_testing
to2.1.12
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You also need to set channel to
nightly-2023-08-10
inrust-toolchain.toml
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
I updated
clippy_utils
version to1d334696587ac22b3a9e651e7ac684ac9e0697b2
and set channel tonightly-2023-07-14
inrust-toolchain.toml
.nightly-2023-07-14
is required, since this version is used inrust-clippy
at commit1d334696587ac22b3a9e651e7ac684ac9e0697b2
.How should we choose the
clippy
version forlinting
? Should we always take the latest version from theclippy
master
branch?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what you mean by
clippy
here. If you are referring toclippy_utils
, I've been following a template in dylint repoThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By
clippy
I mean the rust-clippy repository, which contains theclippy_utils
crate. In this repo they set the toolchain version tonightly-2023-07-14
for all the crates. So we need to set the same version in our toolchain file to build clippy.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK we only have
rust-toolchain.toml
for the lining crate