use std::collections::HashMap;
use crate::types::ErrorCode;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct RawEntry {
code: ErrorCode,
action: Option<RawAction>,
}
#[derive(Debug, Deserialize)]
struct RawAction {
kind: String,
title: Option<String>,
text: Option<String>,
anchor: Option<String>,
replacement: Option<String>,
node: Option<String>,
walk_to: Option<String>,
before_child: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct CodeActionDef {
pub title: String,
pub fix: FixKind,
}
#[derive(Debug, Clone)]
pub enum FixKind {
Insert { text: String, anchor: InsertAnchor },
ReplaceToken {
replacement: String,
walk_to: Option<String>,
},
DeleteToken,
DeleteNode { node_kind: String },
DeleteChildNode {
walk_to: String,
child_kinds: Vec<String>,
},
ReplaceChildNode {
walk_to: String,
child_kind: String,
replacement: String,
},
InsertBeforeNode {
walk_to: String,
before_child: Vec<String>,
text: String,
},
Custom,
}
#[derive(Debug, Clone)]
pub enum InsertAnchor {
FileStart,
}
static ERROR_CODES_JSON: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/data/error_codes.json"
));
pub fn load() -> HashMap<ErrorCode, CodeActionDef> {
let raw: Vec<RawEntry> =
serde_json::from_str(ERROR_CODES_JSON).expect("data/error_codes.json is malformed");
let mut map = HashMap::new();
for entry in raw {
let Some(action) = entry.action else { continue };
let Some(def) = parse_action(action) else {
continue;
};
map.insert(entry.code, def);
}
map
}
fn parse_action(a: RawAction) -> Option<CodeActionDef> {
let title = a.title.unwrap_or_default();
let fix = match a.kind.as_str() {
"insert" => FixKind::Insert {
text: a.text?,
anchor: match a.anchor.as_deref() {
Some("file_start") | None => InsertAnchor::FileStart,
other => {
eprintln!("unknown insert anchor: {other:?}");
return None;
}
},
},
"replace_token" => FixKind::ReplaceToken {
replacement: a.replacement?,
walk_to: a.walk_to,
},
"delete_token" => FixKind::DeleteToken,
"delete_node" => FixKind::DeleteNode { node_kind: a.node? },
"delete_child_node" => {
let child_kinds = a.before_child.or_else(|| a.node.map(|n| vec![n]))?;
FixKind::DeleteChildNode {
walk_to: a.walk_to?,
child_kinds,
}
}
"replace_child_node" => FixKind::ReplaceChildNode {
walk_to: a.walk_to?,
child_kind: a.node?,
replacement: a.replacement?,
},
"insert_before_node" => FixKind::InsertBeforeNode {
walk_to: a.walk_to?,
before_child: a.before_child?,
text: a.text?,
},
"custom" => FixKind::Custom,
other => {
eprintln!("unknown action kind: {other:?}");
return None;
}
};
Some(CodeActionDef { title, fix })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_parses_without_panic() {
let db = load();
assert!(!db.is_empty(), "database should have at least one action");
}
#[test]
fn test_known_codes_present() {
let db = load();
let expected = [
1878u32, 2072, 2074, 7591, 1827, 9102, 9125, 2662, 6879, 9348, 5424, 7359, 3557, 4538,
8050, 1400, 2256, 8113, 2462, 9239, 8295, 1845, 9559, 7708, 5587, 1560, 1159, 4095, 7341, 8063, 4126,
];
for code in expected {
assert!(db.contains_key(&code), "missing code {code}");
}
}
#[test]
fn test_1878_is_insert() {
let db = load();
let def = db.get(&1878).unwrap();
assert!(def.title.contains("SPDX"));
assert!(matches!(def.fix, FixKind::Insert { .. }));
if let FixKind::Insert { text, anchor } = &def.fix {
assert!(text.contains("SPDX-License-Identifier"));
assert!(matches!(anchor, InsertAnchor::FileStart));
}
}
#[test]
fn test_7359_is_replace_token() {
let db = load();
let def = db.get(&7359).unwrap();
if let FixKind::ReplaceToken { replacement, .. } = &def.fix {
assert_eq!(replacement, "block.timestamp");
} else {
panic!("expected ReplaceToken for 7359");
}
}
#[test]
fn test_2072_is_delete_node() {
let db = load();
let def = db.get(&2072).unwrap();
if let FixKind::DeleteNode { node_kind } = &def.fix {
assert_eq!(node_kind, "variable_declaration_statement");
} else {
panic!("expected DeleteNode for 2072");
}
}
#[test]
fn test_5424_is_insert_before_node() {
let db = load();
let def = db.get(&5424).unwrap();
if let FixKind::InsertBeforeNode {
walk_to,
before_child,
text,
} = &def.fix
{
assert_eq!(walk_to, "function_definition");
assert!(before_child.contains(&"return_type_definition".to_string()));
assert!(before_child.contains(&";".to_string()));
assert_eq!(text, "virtual ");
} else {
panic!("expected InsertBeforeNode for 5424");
}
}
#[test]
fn test_custom_codes_are_custom() {
let db = load();
for code in [2018u32, 9456] {
let def = db.get(&code).unwrap();
assert!(
matches!(def.fix, FixKind::Custom),
"code {code} should be Custom"
);
}
}
#[test]
fn test_null_actions_not_in_map() {
let db = load();
assert!(!db.contains_key(&1005));
}
}