rustledger_plugin/native/plugins/
no_unused.rs1use crate::types::{
4 DirectiveData, MetaValueData, PluginError, PluginInput, PluginOp, PluginOutput,
5};
6
7use super::super::NativePlugin;
8
9pub struct NoUnusedPlugin;
14
15impl NativePlugin for NoUnusedPlugin {
16 fn name(&self) -> &'static str {
17 "nounused"
18 }
19
20 fn description(&self) -> &'static str {
21 "Warn about unused accounts"
22 }
23
24 fn process(&self, input: PluginInput) -> PluginOutput {
25 use std::collections::HashSet;
26
27 let mut opened_accounts: HashSet<String> = HashSet::new();
28 let mut used_accounts: HashSet<String> = HashSet::new();
29
30 for wrapper in &input.directives {
32 match &wrapper.data {
33 DirectiveData::Open(data) => {
34 opened_accounts.insert(data.account.clone());
35 }
36 DirectiveData::Close(data) => {
37 used_accounts.insert(data.account.clone());
39 }
40 DirectiveData::Transaction(txn) => {
41 for posting in &txn.postings {
42 used_accounts.insert(posting.account.clone());
43 }
44 }
45 DirectiveData::Balance(data) => {
46 used_accounts.insert(data.account.clone());
47 }
48 DirectiveData::Pad(data) => {
49 used_accounts.insert(data.account.clone());
50 used_accounts.insert(data.source_account.clone());
51 }
52 DirectiveData::Note(data) => {
53 used_accounts.insert(data.account.clone());
54 }
55 DirectiveData::Document(data) => {
56 used_accounts.insert(data.account.clone());
57 }
58 DirectiveData::Custom(data) => {
59 for value in &data.values {
61 if let MetaValueData::Account(account) = value {
62 used_accounts.insert(account.clone());
63 }
64 }
65 }
66 _ => {}
67 }
68 }
69
70 let mut errors = Vec::new();
72 let mut unused: Vec<_> = opened_accounts
73 .difference(&used_accounts)
74 .cloned()
75 .collect();
76 unused.sort(); for account in unused {
79 errors.push(PluginError::warning(format!(
80 "Account '{account}' is opened but never used"
81 )));
82 }
83
84 PluginOutput {
85 ops: (0..input.directives.len()).map(PluginOp::Keep).collect(),
86 errors,
87 }
88 }
89}
90
91#[cfg(test)]
92mod nounused_tests {
93 use super::*;
94 use crate::types::*;
95
96 #[test]
97 fn test_nounused_reports_unused_account() {
98 let plugin = NoUnusedPlugin;
99
100 let input = PluginInput {
101 directives: vec![
102 DirectiveWrapper {
103 directive_type: "open".to_string(),
104 date: "2024-01-01".to_string(),
105 filename: None,
106 lineno: None,
107 data: DirectiveData::Open(OpenData {
108 account: "Assets:Bank".to_string(),
109 currencies: vec![],
110 booking: None,
111 metadata: vec![],
112 }),
113 },
114 DirectiveWrapper {
115 directive_type: "open".to_string(),
116 date: "2024-01-01".to_string(),
117 filename: None,
118 lineno: None,
119 data: DirectiveData::Open(OpenData {
120 account: "Assets:Unused".to_string(),
121 currencies: vec![],
122 booking: None,
123 metadata: vec![],
124 }),
125 },
126 DirectiveWrapper {
127 directive_type: "transaction".to_string(),
128 date: "2024-01-15".to_string(),
129 filename: None,
130 lineno: None,
131 data: DirectiveData::Transaction(TransactionData {
132 flag: "*".to_string(),
133 payee: None,
134 narration: "Test".to_string(),
135 tags: vec![],
136 links: vec![],
137 metadata: vec![],
138 postings: vec![PostingData {
139 account: "Assets:Bank".to_string(),
140 units: Some(AmountData {
141 number: "100".to_string(),
142 currency: "USD".to_string(),
143 }),
144 cost: None,
145 price: None,
146 flag: None,
147 metadata: vec![],
148 }],
149 }),
150 },
151 ],
152 options: PluginOptions {
153 operating_currencies: vec!["USD".to_string()],
154 title: None,
155 },
156 config: None,
157 };
158
159 let output = plugin.process(input);
160 assert_eq!(output.errors.len(), 1);
161 assert!(output.errors[0].message.contains("Assets:Unused"));
162 assert!(output.errors[0].message.contains("never used"));
163 }
164
165 #[test]
166 fn test_nounused_no_warning_for_used_accounts() {
167 let plugin = NoUnusedPlugin;
168
169 let input = PluginInput {
170 directives: vec![
171 DirectiveWrapper {
172 directive_type: "open".to_string(),
173 date: "2024-01-01".to_string(),
174 filename: None,
175 lineno: None,
176 data: DirectiveData::Open(OpenData {
177 account: "Assets:Bank".to_string(),
178 currencies: vec![],
179 booking: None,
180 metadata: vec![],
181 }),
182 },
183 DirectiveWrapper {
184 directive_type: "transaction".to_string(),
185 date: "2024-01-15".to_string(),
186 filename: None,
187 lineno: None,
188 data: DirectiveData::Transaction(TransactionData {
189 flag: "*".to_string(),
190 payee: None,
191 narration: "Test".to_string(),
192 tags: vec![],
193 links: vec![],
194 metadata: vec![],
195 postings: vec![PostingData {
196 account: "Assets:Bank".to_string(),
197 units: Some(AmountData {
198 number: "100".to_string(),
199 currency: "USD".to_string(),
200 }),
201 cost: None,
202 price: None,
203 flag: None,
204 metadata: vec![],
205 }],
206 }),
207 },
208 ],
209 options: PluginOptions {
210 operating_currencies: vec!["USD".to_string()],
211 title: None,
212 },
213 config: None,
214 };
215
216 let output = plugin.process(input);
217 assert_eq!(output.errors.len(), 0);
218 }
219
220 #[test]
221 fn test_nounused_close_counts_as_used() {
222 let plugin = NoUnusedPlugin;
223
224 let input = PluginInput {
225 directives: vec![
226 DirectiveWrapper {
227 directive_type: "open".to_string(),
228 date: "2024-01-01".to_string(),
229 filename: None,
230 lineno: None,
231 data: DirectiveData::Open(OpenData {
232 account: "Assets:OldAccount".to_string(),
233 currencies: vec![],
234 booking: None,
235 metadata: vec![],
236 }),
237 },
238 DirectiveWrapper {
239 directive_type: "close".to_string(),
240 date: "2024-12-31".to_string(),
241 filename: None,
242 lineno: None,
243 data: DirectiveData::Close(CloseData {
244 account: "Assets:OldAccount".to_string(),
245 metadata: vec![],
246 }),
247 },
248 ],
249 options: PluginOptions {
250 operating_currencies: vec!["USD".to_string()],
251 title: None,
252 },
253 config: None,
254 };
255
256 let output = plugin.process(input);
257 assert_eq!(output.errors.len(), 0);
259 }
260}