Skip to main content

cairo_lint/lints/
enum_variant_names.rs

1use 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
16/// ## What it does
17///
18/// Detects enumeration variants that are prefixed or suffixed by the same characters.
19///
20/// ## Example
21///
22/// ```cairo
23/// enum Cake {
24///     BlackForestCake,
25///     HummingbirdCake,
26///     BattenbergCake,
27/// }
28/// ```
29///
30/// Can be simplified to:
31///
32/// ```cairo
33/// enum Cake {
34///     BlackForest,
35///     Hummingbird,
36///     Battenberg,
37/// }
38/// ```
39impl 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}