Skip to content

Commit 7068bdf

Browse files
committed
implement 6-duplicate-mutable-accounts lint
1 parent 4e1da73 commit 7068bdf

File tree

11 files changed

+2818
-28
lines changed

11 files changed

+2818
-28
lines changed

crate/src/paths.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ pub const ANCHOR_LANG_SIGNER: [&str; 4] = ["anchor_lang", "accounts", "signer",
33

44
pub const SOLANA_PROGRAM_ACCOUNT_INFO: [&str; 3] =
55
["solana_program", "account_info", "AccountInfo"];
6+
pub const ANCHOR_ACCOUNT: [&str; 4] = ["anchor_lang", "accounts", "account", "Account"];
67
pub const BORSH_TRY_FROM_SLICE: [&str; 4] = ["borsh", "de", "BorshDeserialize", "try_from_slice"];

lints/duplicate-mutable-accounts/Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,25 @@ publish = false
99
[lib]
1010
crate-type = ["cdylib"]
1111

12+
[[example]]
13+
name = "insecure"
14+
path = "ui/insecure/src/lib.rs"
15+
16+
[[example]]
17+
name = "secure"
18+
path = "ui/secure/src/lib.rs"
19+
1220
[dependencies]
1321
clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "7b2896a8fc9f0b275692677ee6d2d66a7cbde16a" }
1422
dylint_linting = "2.0.1"
1523
if_chain = "1.0.2"
24+
solana-lints = { path = "../../crate" }
1625

1726
[dev-dependencies]
1827
dylint_testing = "2.0.1"
28+
anchor-lang = "0.24.2"
29+
30+
[workspace]
1931

2032
[package.metadata.rust-analyzer]
2133
rustc_private = true

lints/duplicate-mutable-accounts/src/lib.rs

Lines changed: 140 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,26 @@
22
#![warn(unused_extern_crates)]
33

44
extern crate rustc_ast;
5-
extern crate rustc_ast_pretty;
6-
extern crate rustc_data_structures;
7-
extern crate rustc_errors;
85
extern crate rustc_hir;
9-
extern crate rustc_hir_pretty;
10-
extern crate rustc_index;
11-
extern crate rustc_infer;
12-
extern crate rustc_lexer;
13-
extern crate rustc_middle;
14-
extern crate rustc_mir_dataflow;
15-
extern crate rustc_parse;
16-
extern crate rustc_parse_format;
176
extern crate rustc_span;
18-
extern crate rustc_target;
19-
extern crate rustc_trait_selection;
20-
extern crate rustc_typeck;
217

22-
use rustc_lint::LateLintPass;
8+
use rustc_ast::{
9+
token::TokenKind,
10+
tokenstream::{TokenStream, TokenTree},
11+
AttrKind, Attribute, MacArgs,
12+
};
13+
use rustc_hir::def::Res;
14+
use rustc_hir::*;
15+
use rustc_lint::{LateContext, LateLintPass};
16+
use rustc_span::{def_id::DefId, symbol::Symbol, Span};
17+
use std::collections::HashMap;
18+
use std::default::Default;
2319

24-
dylint_linting::declare_late_lint! {
20+
use clippy_utils::{diagnostics::span_lint_and_help, ty::match_type};
21+
use if_chain::if_chain;
22+
use solana_lints::paths;
23+
24+
dylint_linting::impl_late_lint! {
2525
/// **What it does:**
2626
///
2727
/// **Why is this bad?**
@@ -39,18 +39,134 @@ dylint_linting::declare_late_lint! {
3939
/// ```
4040
pub DUPLICATE_MUTABLE_ACCOUNTS,
4141
Warn,
42-
"description goes here"
42+
"description goes here",
43+
DuplicateMutableAccounts::default()
44+
}
45+
46+
#[derive(Default, Debug)]
47+
struct DuplicateMutableAccounts {
48+
accounts: HashMap<DefId, Vec<(Symbol, Span)>>,
49+
streams: Vec<Stream>,
4350
}
4451

4552
impl<'tcx> LateLintPass<'tcx> for DuplicateMutableAccounts {
46-
// A list of things you might check can be found here:
47-
// https://doc.rust-lang.org/stable/nightly-rustc/rustc_lint/trait.LateLintPass.html
53+
fn check_struct_def(&mut self, cx: &LateContext<'tcx>, variant_data: &'tcx VariantData<'tcx>) {
54+
if let VariantData::Struct(fields, _) = variant_data {
55+
fields.iter().for_each(|field| {
56+
if_chain! {
57+
// grab the def_id of the field type
58+
let ty = field.ty;
59+
if let TyKind::Path(qpath) = &ty.kind;
60+
if let QPath::Resolved(_, path) = qpath;
61+
if let Res::Def(_, def_id) = path.res;
62+
// match the type of the field
63+
let ty = cx.tcx.type_of(def_id);
64+
// check it is an anchor account type
65+
if match_type(cx, ty, &paths::ANCHOR_ACCOUNT);
66+
// check the type of T, the second generic arg
67+
let account_id = get_anchor_account_type(&path.segments[0]).unwrap();
68+
then {
69+
if let Some(v) = self.accounts.get_mut(&account_id) {
70+
v.push((field.ident.name, field.span));
71+
} else {
72+
self.accounts.insert(account_id, vec![(field.ident.name, field.span)]);
73+
}
74+
}
75+
}
76+
});
77+
}
78+
}
79+
80+
fn check_attribute(&mut self, _: &LateContext<'tcx>, attribute: &'tcx Attribute) {
81+
// println!("{:#?}", self.accounts);
82+
if_chain! {
83+
if let AttrKind::Normal(attr_item, _) = &attribute.kind;
84+
let name = attr_item.path.segments[0].ident.name;
85+
// for some reason #[account] doesn't match when no args, maybe take away
86+
// the code to check name, and just check it has constraint args?
87+
if name.as_str() == "account";
88+
if let MacArgs::Delimited(_, _, token_stream) = &attr_item.args;
89+
then {
90+
self.streams.push(Stream(token_stream.clone()));
91+
// println!("{:#?}", attribute);
92+
}
93+
}
94+
}
95+
96+
fn check_crate_post(&mut self, cx: &LateContext<'tcx>) {
97+
// println!("{:#?}", self);
98+
for (_k, v) in self.accounts.iter() {
99+
// if multiple same accounts, check that there is a tokenstream such that
100+
// all necessary expressions are in it: x.key(), y.key(), !=
101+
// where x and y are the field name from self.accounts
102+
if v.len() > 1 {
103+
if !has_satisfying_stream(&self.streams, v) {
104+
span_lint_and_help(
105+
cx,
106+
DUPLICATE_MUTABLE_ACCOUNTS,
107+
v[0].1,
108+
"identical account types",
109+
Some(v[1].1),
110+
&format!("add an anchor key check constraint: #[account(constraint = {}.key() != {}.key())]", v[0].0, v[1].0)
111+
);
112+
}
113+
}
114+
}
115+
}
116+
}
117+
118+
fn get_anchor_account_type(segment: &PathSegment<'_>) -> Option<DefId> {
119+
if_chain! {
120+
if let Some(generic_args) = segment.args;
121+
if let GenericArg::Type(ty) = &generic_args.args[1]; // the account type is the second generic arg
122+
if let TyKind::Path(qpath) = &ty.kind;
123+
if let QPath::Resolved(_, path) = qpath;
124+
if let Res::Def(_, def_id) = path.res;
125+
then {
126+
Some(def_id)
127+
} else {
128+
None
129+
}
130+
}
131+
}
132+
133+
fn has_satisfying_stream(streams: &Vec<Stream>, field_names: &Vec<(Symbol, Span)>) -> bool {
134+
for stream in streams {
135+
if stream.contains(TokenKind::Ne)
136+
&& field_names
137+
.iter()
138+
// TODO: if true, will not match. figure out what the bool signifies
139+
.all(|(sym, _)| stream.contains(TokenKind::Ident(*sym, false)))
140+
{
141+
return true;
142+
}
143+
}
144+
return false;
145+
}
146+
147+
#[derive(Debug)]
148+
pub struct Stream(TokenStream);
149+
150+
impl Stream {
151+
fn contains(&self, x: TokenKind) -> bool {
152+
for token_tree in self.0.trees() {
153+
if let TokenTree::Token(t) = token_tree {
154+
if t.kind == x {
155+
// println!("The type {:?} matches {:?}", t.kind, x);
156+
return true;
157+
}
158+
}
159+
}
160+
return false;
161+
}
162+
}
163+
164+
#[test]
165+
fn insecure() {
166+
dylint_testing::ui_test_example(env!("CARGO_PKG_NAME"), "insecure");
48167
}
49168

50169
#[test]
51-
fn ui() {
52-
dylint_testing::ui_test(
53-
env!("CARGO_PKG_NAME"),
54-
&std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("ui"),
55-
);
170+
fn secure() {
171+
dylint_testing::ui_test_example(env!("CARGO_PKG_NAME"), "secure");
56172
}

0 commit comments

Comments
 (0)