solidity_language_server/
code_actions.rs1use std::collections::HashMap;
37
38use serde::Deserialize;
39
40#[derive(Debug, Deserialize)]
43struct RawEntry {
44 code: u32,
45 action: Option<RawAction>,
46}
47
48#[derive(Debug, Deserialize)]
49struct RawAction {
50 kind: String,
51 title: Option<String>,
52 text: Option<String>,
54 anchor: Option<String>,
55 replacement: Option<String>,
57 node: Option<String>,
59 walk_to: Option<String>,
61 before_child: Option<Vec<String>>,
62}
63
64#[derive(Debug, Clone)]
68pub struct CodeActionDef {
69 pub title: String,
70 pub fix: FixKind,
71}
72
73#[derive(Debug, Clone)]
74pub enum FixKind {
75 Insert { text: String, anchor: InsertAnchor },
77
78 ReplaceToken {
82 replacement: String,
83 walk_to: Option<String>,
84 },
85
86 DeleteToken,
88
89 DeleteNode { node_kind: String },
92
93 DeleteChildNode {
98 walk_to: String,
99 child_kinds: Vec<String>,
100 },
101
102 ReplaceChildNode {
106 walk_to: String,
107 child_kind: String,
108 replacement: String,
109 },
110
111 InsertBeforeNode {
114 walk_to: String,
115 before_child: Vec<String>,
116 text: String,
117 },
118
119 Custom,
122}
123
124#[derive(Debug, Clone)]
125pub enum InsertAnchor {
126 FileStart,
127}
128
129static ERROR_CODES_JSON: &str = include_str!(concat!(
132 env!("CARGO_MANIFEST_DIR"),
133 "/data/error_codes.json"
134));
135
136pub fn load() -> HashMap<u32, CodeActionDef> {
140 let raw: Vec<RawEntry> =
141 serde_json::from_str(ERROR_CODES_JSON).expect("data/error_codes.json is malformed");
142
143 let mut map = HashMap::new();
144 for entry in raw {
145 let Some(action) = entry.action else { continue };
146 let Some(def) = parse_action(action) else {
147 continue;
148 };
149 map.insert(entry.code, def);
150 }
151 map
152}
153
154fn parse_action(a: RawAction) -> Option<CodeActionDef> {
155 let title = a.title.unwrap_or_default();
156 let fix = match a.kind.as_str() {
157 "insert" => FixKind::Insert {
158 text: a.text?,
159 anchor: match a.anchor.as_deref() {
160 Some("file_start") | None => InsertAnchor::FileStart,
161 other => {
162 eprintln!("unknown insert anchor: {other:?}");
163 return None;
164 }
165 },
166 },
167
168 "replace_token" => FixKind::ReplaceToken {
169 replacement: a.replacement?,
170 walk_to: a.walk_to,
171 },
172
173 "delete_token" => FixKind::DeleteToken,
174
175 "delete_node" => FixKind::DeleteNode { node_kind: a.node? },
176
177 "delete_child_node" => {
178 let child_kinds = a.before_child.or_else(|| a.node.map(|n| vec![n]))?;
181 FixKind::DeleteChildNode {
182 walk_to: a.walk_to?,
183 child_kinds,
184 }
185 }
186
187 "replace_child_node" => FixKind::ReplaceChildNode {
188 walk_to: a.walk_to?,
189 child_kind: a.node?,
190 replacement: a.replacement?,
191 },
192
193 "insert_before_node" => FixKind::InsertBeforeNode {
194 walk_to: a.walk_to?,
195 before_child: a.before_child?,
196 text: a.text?,
197 },
198
199 "custom" => FixKind::Custom,
200
201 other => {
202 eprintln!("unknown action kind: {other:?}");
203 return None;
204 }
205 };
206
207 Some(CodeActionDef { title, fix })
208}
209
210#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn test_load_parses_without_panic() {
218 let db = load();
219 assert!(!db.is_empty(), "database should have at least one action");
220 }
221
222 #[test]
223 fn test_known_codes_present() {
224 let db = load();
225 let expected = [
227 1878u32, 2072, 2074, 7591, 1827, 9102, 9125, 2662, 6879, 9348, 5424, 7359, 3557, 4538,
228 8050, 1400, 2256, 8113, 2462, 9239, 8295, 1845, 9559, 7708, 5587, 1560, 1159, 4095, 7341, 8063, 4126,
234 ];
235 for code in expected {
236 assert!(db.contains_key(&code), "missing code {code}");
237 }
238 }
239
240 #[test]
241 fn test_1878_is_insert() {
242 let db = load();
243 let def = db.get(&1878).unwrap();
244 assert!(def.title.contains("SPDX"));
245 assert!(matches!(def.fix, FixKind::Insert { .. }));
246 if let FixKind::Insert { text, anchor } = &def.fix {
247 assert!(text.contains("SPDX-License-Identifier"));
248 assert!(matches!(anchor, InsertAnchor::FileStart));
249 }
250 }
251
252 #[test]
253 fn test_7359_is_replace_token() {
254 let db = load();
255 let def = db.get(&7359).unwrap();
256 if let FixKind::ReplaceToken { replacement, .. } = &def.fix {
257 assert_eq!(replacement, "block.timestamp");
258 } else {
259 panic!("expected ReplaceToken for 7359");
260 }
261 }
262
263 #[test]
264 fn test_2072_is_delete_node() {
265 let db = load();
266 let def = db.get(&2072).unwrap();
267 if let FixKind::DeleteNode { node_kind } = &def.fix {
268 assert_eq!(node_kind, "variable_declaration_statement");
269 } else {
270 panic!("expected DeleteNode for 2072");
271 }
272 }
273
274 #[test]
275 fn test_5424_is_insert_before_node() {
276 let db = load();
277 let def = db.get(&5424).unwrap();
278 if let FixKind::InsertBeforeNode {
279 walk_to,
280 before_child,
281 text,
282 } = &def.fix
283 {
284 assert_eq!(walk_to, "function_definition");
285 assert!(before_child.contains(&"return_type_definition".to_string()));
286 assert!(before_child.contains(&";".to_string()));
287 assert_eq!(text, "virtual ");
288 } else {
289 panic!("expected InsertBeforeNode for 5424");
290 }
291 }
292
293 #[test]
294 fn test_custom_codes_are_custom() {
295 let db = load();
296 for code in [2018u32, 9456] {
297 let def = db.get(&code).unwrap();
298 assert!(
299 matches!(def.fix, FixKind::Custom),
300 "code {code} should be Custom"
301 );
302 }
303 }
304
305 #[test]
306 fn test_null_actions_not_in_map() {
307 let db = load();
308 assert!(!db.contains_key(&1005));
310 }
311}