forge_lint/sol/info/
mixed_case.rs1use super::{MixedCaseFunction, MixedCaseVariable};
2use crate::{
3 linter::{EarlyLintPass, LintContext},
4 sol::{Severity, SolLint, info::screaming_snake_case::is_screaming_snake_case},
5};
6use solar::ast::{FunctionHeader, ItemFunction, VariableDefinition, Visibility};
7
8declare_forge_lint!(
9 MIXED_CASE_FUNCTION,
10 Severity::Info,
11 "mixed-case-function",
12 "function names should use mixedCase"
13);
14
15impl<'ast> EarlyLintPass<'ast> for MixedCaseFunction {
16 fn check_item_function(&mut self, ctx: &LintContext, func: &'ast ItemFunction<'ast>) {
17 if let Some(name) = func.header.name
18 && !is_mixed_case(name.as_str(), true, ctx.config.mixed_case_exceptions)
19 && !is_constant_getter(&func.header)
20 {
21 ctx.emit(&MIXED_CASE_FUNCTION, name.span);
22 }
23 }
24}
25
26declare_forge_lint!(
27 MIXED_CASE_VARIABLE,
28 Severity::Info,
29 "mixed-case-variable",
30 "mutable variables should use mixedCase"
31);
32
33impl<'ast> EarlyLintPass<'ast> for MixedCaseVariable {
34 fn check_variable_definition(
35 &mut self,
36 ctx: &LintContext,
37 var: &'ast VariableDefinition<'ast>,
38 ) {
39 if var.mutability.is_none()
40 && let Some(name) = var.name
41 && !is_mixed_case(name.as_str(), false, ctx.config.mixed_case_exceptions)
42 {
43 ctx.emit(&MIXED_CASE_VARIABLE, name.span);
44 }
45 }
46}
47
48pub fn is_mixed_case(s: &str, is_fn: bool, allowed_patterns: &[String]) -> bool {
53 if s.len() <= 1 {
54 return true;
55 }
56
57 if check_lower_mixed_case(s.trim_matches('_')) {
59 return true;
60 }
61
62 for pattern in allowed_patterns {
64 if let Some(pos) = s.find(pattern.as_str())
65 && check_lower_mixed_case(&s[..pos])
66 && check_upper_mixed_case_post_pattern(&s[pos + pattern.len()..])
67 {
68 return true;
69 }
70 }
71
72 is_fn && (s.starts_with("test") || s.starts_with("invariant_") || s.starts_with("statefulFuzz"))
75}
76
77fn check_lower_mixed_case(s: &str) -> bool {
78 s == heck::AsLowerCamelCase(s).to_string().as_str()
79}
80
81fn check_upper_mixed_case_post_pattern(s: &str) -> bool {
82 let Some(split_idx) = s.find(|c: char| !c.is_numeric()) else {
84 return true;
85 };
86
87 let trimmed = &s[split_idx..];
89 if let Some(c) = trimmed.chars().next()
90 && !c.is_alphabetic()
91 {
92 return false;
93 }
94 trimmed == heck::AsUpperCamelCase(trimmed).to_string().as_str()
95}
96
97fn is_constant_getter(header: &FunctionHeader<'_>) -> bool {
104 header.visibility().is_some_and(|v| matches!(v, Visibility::External))
105 && header.state_mutability().is_view()
106 && header.parameters.is_empty()
107 && header.returns().len() == 1
108 && header.returns().first().is_some_and(|ret| ret.ty.kind.is_elementary())
109 && is_screaming_snake_case(header.name.unwrap().as_str())
110}