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