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 crate::types::ErrorCode;
39use serde::Deserialize;
40
41// ── JSON types ───────────────────────────────────────────────────────────────
42
43#[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    // insert / insert_before_node
54    text: Option<String>,
55    anchor: Option<String>,
56    // replace_token
57    replacement: Option<String>,
58    // delete_node
59    node: Option<String>,
60    // insert_before_node
61    walk_to: Option<String>,
62    before_child: Option<Vec<String>>,
63}
64
65// ── Public types ─────────────────────────────────────────────────────────────
66
67/// A fully-typed quick-fix action loaded from the database.
68#[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 fixed text at a well-known anchor.
77    Insert { text: String, anchor: InsertAnchor },
78
79    /// Replace the token whose byte range starts at `diag_range.start`.
80    /// When `walk_to` is `Some`, walk up the TS tree to the first ancestor of
81    /// that kind and replace that whole node instead of just the leaf token.
82    ReplaceToken {
83        replacement: String,
84        walk_to: Option<String>,
85    },
86
87    /// Delete the token at `diag_range.start` (+ one trailing space if present).
88    DeleteToken,
89
90    /// Walk the TS tree up to `node_kind`, then delete the whole node
91    /// (including leading whitespace/newline so the line disappears cleanly).
92    DeleteNode { node_kind: String },
93
94    /// Walk the TS tree up to `walk_to`, then delete the first child whose
95    /// kind matches any entry in `child_kinds` (tried in order).
96    /// Used when the diagnostic points to the parent node (e.g. 4126: diag
97    /// starts at `function` keyword but we need to delete the `visibility` child).
98    DeleteChildNode {
99        walk_to: String,
100        child_kinds: Vec<String>,
101    },
102
103    /// Walk the TS tree up to `walk_to`, then replace the first child whose
104    /// kind matches `child_kind` with `replacement`.
105    /// Used for 1560/1159/4095: replace wrong visibility with `external`.
106    ReplaceChildNode {
107        walk_to: String,
108        child_kind: String,
109        replacement: String,
110    },
111
112    /// Walk the TS tree up to `walk_to`, then insert `text` immediately before
113    /// the first child whose kind matches any entry in `before_child`.
114    InsertBeforeNode {
115        walk_to: String,
116        before_child: Vec<String>,
117        text: String,
118    },
119
120    /// No generic fix available — the handler falls through to a hand-written
121    /// match arm in `lsp.rs`.
122    Custom,
123}
124
125#[derive(Debug, Clone)]
126pub enum InsertAnchor {
127    FileStart,
128}
129
130// ── Database ─────────────────────────────────────────────────────────────────
131
132static ERROR_CODES_JSON: &str = include_str!(concat!(
133    env!("CARGO_MANIFEST_DIR"),
134    "/data/error_codes.json"
135));
136
137/// Parse the embedded JSON once and return a map from error code → action.
138/// Entries whose `action` is `null` are omitted from the map.
139/// Call this once at server startup and store the result.
140pub 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            // `before_child` is an ordered list of candidate kinds (first match wins).
180            // `node` is a single-kind shorthand; if both present, before_child wins.
181            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// ── Tests ────────────────────────────────────────────────────────────────────
212
213#[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        // Every code we explicitly handle should be in the map.
227        let expected = [
228            1878u32, 2072, 2074, 7591, 1827, 9102, 9125, 2662, 6879, 9348, 5424, 7359, 3557, 4538,
229            8050, 1400, 2256, 8113, // constructor visibility
230            2462, 9239, 8295, 1845, // payable
231            9559, 7708, 5587, // interface/fallback/receive must be external
232            1560, 1159, 4095, 7341, // modifier virtual
233            8063, // free function visibility (fixed kind)
234            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        // 1005 has no action in the JSON
310        assert!(!db.contains_key(&1005));
311    }
312}