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