Skip to main content

solidity_language_server/
code_actions.rs

1/// JSON-driven code-action database.
2///
3/// The database is compiled into the binary via `include_str!` so there is no
4/// runtime I/O and no additional install step for users.
5///
6/// # Schema
7///
8/// Each entry in `data/error_codes.json` may carry an `"action"` object.
9/// `null` means no quick-fix is available.  A non-null object has the shape:
10///
11/// ```json
12/// {
13///   "kind":  "insert" | "replace_token" | "delete_token" |
14///            "delete_node" | "insert_before_node" | "custom",
15///   "title": "<human-readable label shown in the editor>",
16///
17///   // insert only
18///   "text":   "<text to insert>",
19///   "anchor": "file_start",          // only value for now
20///
21///   // replace_token only
22///   "replacement": "<new text>",
23///
24///   // delete_node only
25///   "node": "<tree-sitter node kind to delete>",
26///
27///   // insert_before_node only
28///   "walk_to":      "<tree-sitter node kind to walk up to>",
29///   "before_child": ["<first matching child kind>", ...],
30///   "text":         "<text to insert>"
31/// }
32/// ```
33///
34/// `"custom"` entries have no extra fields — the handler falls through to the
35/// hand-written match arms in `lsp.rs`.
36use std::collections::HashMap;
37
38use serde::Deserialize;
39
40// ── JSON types ───────────────────────────────────────────────────────────────
41
42#[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    // insert / insert_before_node
53    text: Option<String>,
54    anchor: Option<String>,
55    // replace_token
56    replacement: Option<String>,
57    // delete_node
58    node: Option<String>,
59    // insert_before_node
60    walk_to: Option<String>,
61    before_child: Option<Vec<String>>,
62}
63
64// ── Public types ─────────────────────────────────────────────────────────────
65
66/// A fully-typed quick-fix action loaded from the database.
67#[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 fixed text at a well-known anchor.
76    Insert { text: String, anchor: InsertAnchor },
77
78    /// Replace the token whose byte range starts at `diag_range.start`.
79    /// When `walk_to` is `Some`, walk up the TS tree to the first ancestor of
80    /// that kind and replace that whole node instead of just the leaf token.
81    ReplaceToken {
82        replacement: String,
83        walk_to: Option<String>,
84    },
85
86    /// Delete the token at `diag_range.start` (+ one trailing space if present).
87    DeleteToken,
88
89    /// Walk the TS tree up to `node_kind`, then delete the whole node
90    /// (including leading whitespace/newline so the line disappears cleanly).
91    DeleteNode { node_kind: String },
92
93    /// Walk the TS tree up to `walk_to`, then delete the first child whose
94    /// kind matches any entry in `child_kinds` (tried in order).
95    /// Used when the diagnostic points to the parent node (e.g. 4126: diag
96    /// starts at `function` keyword but we need to delete the `visibility` child).
97    DeleteChildNode {
98        walk_to: String,
99        child_kinds: Vec<String>,
100    },
101
102    /// Walk the TS tree up to `walk_to`, then replace the first child whose
103    /// kind matches `child_kind` with `replacement`.
104    /// Used for 1560/1159/4095: replace wrong visibility with `external`.
105    ReplaceChildNode {
106        walk_to: String,
107        child_kind: String,
108        replacement: String,
109    },
110
111    /// Walk the TS tree up to `walk_to`, then insert `text` immediately before
112    /// the first child whose kind matches any entry in `before_child`.
113    InsertBeforeNode {
114        walk_to: String,
115        before_child: Vec<String>,
116        text: String,
117    },
118
119    /// No generic fix available — the handler falls through to a hand-written
120    /// match arm in `lsp.rs`.
121    Custom,
122}
123
124#[derive(Debug, Clone)]
125pub enum InsertAnchor {
126    FileStart,
127}
128
129// ── Database ─────────────────────────────────────────────────────────────────
130
131static ERROR_CODES_JSON: &str = include_str!(concat!(
132    env!("CARGO_MANIFEST_DIR"),
133    "/data/error_codes.json"
134));
135
136/// Parse the embedded JSON once and return a map from error code → action.
137/// Entries whose `action` is `null` are omitted from the map.
138/// Call this once at server startup and store the result.
139pub 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            // `before_child` is an ordered list of candidate kinds (first match wins).
179            // `node` is a single-kind shorthand; if both present, before_child wins.
180            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// ── Tests ────────────────────────────────────────────────────────────────────
211
212#[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        // Every code we explicitly handle should be in the map.
226        let expected = [
227            1878u32, 2072, 2074, 7591, 1827, 9102, 9125, 2662, 6879, 9348, 5424, 7359, 3557, 4538,
228            8050, 1400, 2256, 8113, // constructor visibility
229            2462, 9239, 8295, 1845, // payable
230            9559, 7708, 5587, // interface/fallback/receive must be external
231            1560, 1159, 4095, 7341, // modifier virtual
232            8063, // free function visibility (fixed kind)
233            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        // 1005 has no action in the JSON
309        assert!(!db.contains_key(&1005));
310    }
311}