cairo_lint/lints/
enum_variant_names.rs1use crate::context::{CairoLintKind, Lint};
2
3use crate::fixer::InternalFix;
4use cairo_lang_defs::ids::{LanguageElementId, ModuleItemId};
5use cairo_lang_defs::plugin::PluginDiagnostic;
6use cairo_lang_diagnostics::Severity;
7use cairo_lang_semantic::items::enm::EnumSemantic;
8
9use cairo_lang_syntax::node::{
10 SyntaxNode, Terminal, TypedSyntaxNode, ast::ItemEnum as AstEnumItem,
11};
12use salsa::Database;
13
14pub struct EnumVariantNames;
15
16impl Lint for EnumVariantNames {
40 fn allowed_name(&self) -> &'static str {
41 "enum_variant_names"
42 }
43
44 fn diagnostic_message(&self) -> &'static str {
45 "All enum variants are prefixed or suffixed by the same characters."
46 }
47
48 fn kind(&self) -> CairoLintKind {
49 CairoLintKind::EnumVariantNames
50 }
51
52 fn is_enabled(&self) -> bool {
53 false
54 }
55
56 fn has_fixer(&self) -> bool {
57 true
58 }
59
60 fn fix<'db>(&self, db: &'db dyn Database, node: SyntaxNode<'db>) -> Option<InternalFix<'db>> {
61 fix_enum_variant_names(db, node)
62 }
63
64 fn fix_message(&self) -> Option<&'static str> {
65 Some("Remove redundant prefix/suffix from enum variants")
66 }
67}
68
69#[tracing::instrument(skip_all, level = "trace")]
70pub fn check_enum_variant_names<'db>(
71 db: &'db dyn Database,
72 item: &ModuleItemId<'db>,
73 diagnostics: &mut Vec<PluginDiagnostic<'db>>,
74) {
75 let ModuleItemId::Enum(enum_id) = item else {
76 return;
77 };
78 let Ok(variants) = db.enum_variants(*enum_id) else {
79 return;
80 };
81 let variant_names: Vec<String> = variants.iter().map(|v| v.0.to_string(db)).collect();
82
83 let (prefix, suffix) = get_prefix_and_suffix(&variant_names);
84
85 if !prefix.is_empty() || !suffix.is_empty() {
86 diagnostics.push(PluginDiagnostic {
87 stable_ptr: enum_id.untyped_stable_ptr(db),
88 message: EnumVariantNames.diagnostic_message().to_string(),
89 severity: Severity::Warning,
90 error_code: None,
91 inner_span: None,
92 });
93 }
94}
95
96#[tracing::instrument(skip_all, level = "trace")]
97fn fix_enum_variant_names<'db>(
98 db: &'db dyn Database,
99 node: SyntaxNode<'db>,
100) -> Option<InternalFix<'db>> {
101 let enum_item = AstEnumItem::from_syntax_node(db, node);
102
103 let source = enum_item.as_syntax_node().get_text(db);
104 let variants = enum_item.variants(db).elements(db);
105
106 let variant_names: Vec<String> = variants
107 .map(|v| v.name(db).text(db).to_string(db))
108 .collect();
109
110 let (prefixes, suffixes) = get_prefix_and_suffix(&variant_names);
111
112 let mut fixed_enum = source.to_string();
113
114 for variant in &variant_names {
115 let mut fixed_name = variant.clone();
116
117 for prefix in &prefixes {
118 if let Some(stripped) = fixed_name.strip_prefix(prefix) {
119 fixed_name = stripped.to_string();
120 }
121 }
122
123 for suffix in &suffixes {
124 if let Some(stripped) = fixed_name.strip_suffix(suffix) {
125 fixed_name = stripped.to_string();
126 }
127 }
128
129 fixed_enum = fixed_enum.replace(variant, &fixed_name);
130 }
131
132 Some(InternalFix {
133 node,
134 suggestion: fixed_enum,
135 description: EnumVariantNames.fix_message().unwrap().to_string(),
136 import_addition_paths: None,
137 })
138}
139
140fn get_prefix_and_suffix(variant_names: &[String]) -> (Vec<String>, Vec<String>) {
141 let Some(first) = variant_names.first() else {
142 return (vec![], vec![]);
143 };
144
145 if variant_names.len() == 1 {
146 return (vec![], vec![]);
147 }
148
149 let mut prefix = word_split(first);
150 let mut suffix = prefix.clone();
151 suffix.reverse();
152
153 for variant_name in variant_names.iter().skip(1) {
154 let variant_split = word_split(variant_name);
155
156 if variant_split.len() == 1 {
157 return (vec![], vec![]);
158 }
159
160 prefix = prefix
161 .iter()
162 .zip(&variant_split)
163 .take_while(|(a, b)| a == b)
164 .map(|(a, _)| a.clone())
165 .collect();
166
167 suffix = suffix
168 .iter()
169 .zip(variant_split.iter().rev())
170 .take_while(|(a, b)| a == b)
171 .map(|(a, _)| a.clone())
172 .collect();
173 }
174
175 (prefix, suffix)
176}
177
178fn word_split(name: &str) -> Vec<String> {
179 let mut parts = Vec::new();
180 let mut start = 0;
181
182 let chars: Vec<char> = name.chars().collect();
183
184 for i in 1..chars.len() {
185 let prev = chars[i - 1];
186 let curr = chars[i];
187
188 if curr.is_uppercase() && prev.is_lowercase() {
189 parts.push(name[start..i].to_string());
190 start = i;
191 } else if curr == '_' {
192 parts.push(name[start..i].to_string());
193 start = i + 1;
194 }
195 }
196
197 if start < name.len() {
198 parts.push(name[start..].to_string());
199 }
200
201 parts
202}