Skip to main content

forge_lint/sol/
naming.rs

1//! Naming-convention helpers shared by Solidity lints.
2//!
3//! Each `check_*` returns `Some(suggestion)` when `s` violates the convention,
4//! `None` when it already matches. Leading/trailing underscores are preserved.
5
6/// `Some(suggestion)` if `s` is not `PascalCase`.
7pub fn check_pascal_case(s: &str) -> Option<String> {
8    if s.len() <= 1 {
9        return None;
10    }
11    let expected = heck::AsPascalCase(s).to_string();
12    if s == expected { None } else { Some(expected) }
13}
14
15/// `Some(suggestion)` if `s` is not `SCREAMING_SNAKE_CASE`.
16pub fn check_screaming_snake_case(s: &str) -> Option<String> {
17    if s.len() <= 1 {
18        return None;
19    }
20    let expected = preserve_underscores(s, heck::AsShoutySnakeCase(s).to_string());
21    if s == expected { None } else { Some(expected) }
22}
23
24/// `Some(suggestion)` if `s` is not `mixedCase`. Pure check — domain
25/// exceptions (test-prefixes, allowed patterns, ...) live in the lint.
26pub fn check_mixed_case(s: &str) -> Option<String> {
27    if s.len() <= 1 {
28        return None;
29    }
30    let expected = preserve_underscores(s, heck::AsLowerCamelCase(s).to_string());
31    if s == expected { None } else { Some(expected) }
32}
33
34fn preserve_underscores(s: &str, body: String) -> String {
35    let prefix = if s.starts_with('_') { "_" } else { "" };
36    let suffix = if s.ends_with('_') { "_" } else { "" };
37    format!("{prefix}{body}{suffix}")
38}
39
40#[cfg(test)]
41mod tests {
42    use super::*;
43
44    #[test]
45    fn pascal_case_accepts_valid() {
46        assert_eq!(check_pascal_case("MyStruct"), None);
47        assert_eq!(check_pascal_case("Erc20"), None);
48        assert_eq!(check_pascal_case("A"), None);
49    }
50
51    #[test]
52    fn pascal_case_suggests_for_invalid() {
53        assert_eq!(check_pascal_case("my_struct").as_deref(), Some("MyStruct"));
54        assert_eq!(check_pascal_case("myStruct").as_deref(), Some("MyStruct"));
55        assert_eq!(check_pascal_case("MY_STRUCT").as_deref(), Some("MyStruct"));
56    }
57
58    #[test]
59    fn screaming_snake_case_accepts_valid() {
60        assert_eq!(check_screaming_snake_case("MAX_VALUE"), None);
61        assert_eq!(check_screaming_snake_case("_PRIVATE_CONST"), None);
62        assert_eq!(check_screaming_snake_case("VALUE_"), None);
63    }
64
65    #[test]
66    fn screaming_snake_case_suggests_for_invalid() {
67        assert_eq!(check_screaming_snake_case("maxValue").as_deref(), Some("MAX_VALUE"));
68        assert_eq!(check_screaming_snake_case("MaxValue").as_deref(), Some("MAX_VALUE"));
69    }
70
71    #[test]
72    fn screaming_snake_case_preserves_underscores() {
73        assert_eq!(check_screaming_snake_case("_maxValue").as_deref(), Some("_MAX_VALUE"));
74        assert_eq!(check_screaming_snake_case("maxValue_").as_deref(), Some("MAX_VALUE_"));
75    }
76
77    #[test]
78    fn mixed_case_accepts_valid() {
79        assert_eq!(check_mixed_case("counter"), None);
80        assert_eq!(check_mixed_case("totalSupply"), None);
81        assert_eq!(check_mixed_case("_internalVar"), None);
82    }
83
84    #[test]
85    fn mixed_case_suggests_for_invalid() {
86        assert_eq!(check_mixed_case("TotalSupply").as_deref(), Some("totalSupply"));
87        assert_eq!(check_mixed_case("total_supply").as_deref(), Some("totalSupply"));
88        assert_eq!(check_mixed_case("TOTAL_SUPPLY").as_deref(), Some("totalSupply"));
89    }
90
91    #[test]
92    fn mixed_case_preserves_underscores() {
93        assert_eq!(check_mixed_case("_TotalSupply").as_deref(), Some("_totalSupply"));
94        assert_eq!(check_mixed_case("totalSupply_"), None);
95    }
96}