rustledger_plugin/native/plugins/
no_unused.rs1use crate::types::{
4 DirectiveData, MetaValueData, PluginError, PluginInput, PluginOp, PluginOutput,
5};
6
7use super::super::{NativePlugin, RegularPlugin};
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
91impl RegularPlugin for NoUnusedPlugin {}
92
93#[cfg(test)]
94mod nounused_tests {
95 use super::*;
96 use crate::types::*;
97
98 #[test]
99 fn test_nounused_reports_unused_account() {
100 let plugin = NoUnusedPlugin;
101
102 let input = PluginInput {
103 directives: vec![
104 DirectiveWrapper {
105 directive_type: "open".to_string(),
106 date: "2024-01-01".to_string(),
107 filename: None,
108 lineno: None,
109 data: DirectiveData::Open(OpenData {
110 account: "Assets:Bank".to_string(),
111 currencies: vec![],
112 booking: None,
113 metadata: vec![],
114 }),
115 },
116 DirectiveWrapper {
117 directive_type: "open".to_string(),
118 date: "2024-01-01".to_string(),
119 filename: None,
120 lineno: None,
121 data: DirectiveData::Open(OpenData {
122 account: "Assets:Unused".to_string(),
123 currencies: vec![],
124 booking: None,
125 metadata: vec![],
126 }),
127 },
128 DirectiveWrapper {
129 directive_type: "transaction".to_string(),
130 date: "2024-01-15".to_string(),
131 filename: None,
132 lineno: None,
133 data: DirectiveData::Transaction(TransactionData {
134 flag: "*".to_string(),
135 payee: None,
136 narration: "Test".to_string(),
137 tags: vec![],
138 links: vec![],
139 metadata: vec![],
140 postings: vec![PostingData {
141 account: "Assets:Bank".to_string(),
142 units: Some(AmountData {
143 number: "100".to_string(),
144 currency: "USD".to_string(),
145 }),
146 cost: None,
147 price: None,
148 flag: None,
149 metadata: vec![],
150 span: None,
151 }],
152 }),
153 },
154 ],
155 options: PluginOptions {
156 operating_currencies: vec!["USD".to_string()],
157 title: None,
158 },
159 config: None,
160 };
161
162 let output = plugin.process(input);
163 assert_eq!(output.errors.len(), 1);
164 assert!(output.errors[0].message.contains("Assets:Unused"));
165 assert!(output.errors[0].message.contains("never used"));
166 }
167
168 #[test]
169 fn test_nounused_no_warning_for_used_accounts() {
170 let plugin = NoUnusedPlugin;
171
172 let input = PluginInput {
173 directives: vec![
174 DirectiveWrapper {
175 directive_type: "open".to_string(),
176 date: "2024-01-01".to_string(),
177 filename: None,
178 lineno: None,
179 data: DirectiveData::Open(OpenData {
180 account: "Assets:Bank".to_string(),
181 currencies: vec![],
182 booking: None,
183 metadata: vec![],
184 }),
185 },
186 DirectiveWrapper {
187 directive_type: "transaction".to_string(),
188 date: "2024-01-15".to_string(),
189 filename: None,
190 lineno: None,
191 data: DirectiveData::Transaction(TransactionData {
192 flag: "*".to_string(),
193 payee: None,
194 narration: "Test".to_string(),
195 tags: vec![],
196 links: vec![],
197 metadata: vec![],
198 postings: vec![PostingData {
199 account: "Assets:Bank".to_string(),
200 units: Some(AmountData {
201 number: "100".to_string(),
202 currency: "USD".to_string(),
203 }),
204 cost: None,
205 price: None,
206 flag: None,
207 metadata: vec![],
208 span: None,
209 }],
210 }),
211 },
212 ],
213 options: PluginOptions {
214 operating_currencies: vec!["USD".to_string()],
215 title: None,
216 },
217 config: None,
218 };
219
220 let output = plugin.process(input);
221 assert_eq!(output.errors.len(), 0);
222 }
223
224 #[test]
225 fn test_nounused_close_counts_as_used() {
226 let plugin = NoUnusedPlugin;
227
228 let input = PluginInput {
229 directives: vec![
230 DirectiveWrapper {
231 directive_type: "open".to_string(),
232 date: "2024-01-01".to_string(),
233 filename: None,
234 lineno: None,
235 data: DirectiveData::Open(OpenData {
236 account: "Assets:OldAccount".to_string(),
237 currencies: vec![],
238 booking: None,
239 metadata: vec![],
240 }),
241 },
242 DirectiveWrapper {
243 directive_type: "close".to_string(),
244 date: "2024-12-31".to_string(),
245 filename: None,
246 lineno: None,
247 data: DirectiveData::Close(CloseData {
248 account: "Assets:OldAccount".to_string(),
249 metadata: vec![],
250 }),
251 },
252 ],
253 options: PluginOptions {
254 operating_currencies: vec!["USD".to_string()],
255 title: None,
256 },
257 config: None,
258 };
259
260 let output = plugin.process(input);
261 assert_eq!(output.errors.len(), 0);
263 }
264}