forge_lint/sol/info/
mixed_case.rs1use super::{MixedCaseFunction, MixedCaseVariable};
2use crate::{
3 linter::{EarlyLintPass, LintContext, Suggestion},
4 sol::{Severity, SolLint, info::screaming_snake_case::check_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 && let Some(expected) =
19 check_mixed_case(name.as_str(), true, ctx.config.mixed_case_exceptions)
20 && !is_constant_getter(&func.header)
21 {
22 ctx.emit_with_suggestion(
23 &MIXED_CASE_FUNCTION,
24 name.span,
25 Suggestion::fix(
26 expected,
27 solar::interface::diagnostics::Applicability::MachineApplicable,
28 )
29 .with_desc("consider using"),
30 );
31 }
32 }
33}
34
35declare_forge_lint!(
36 MIXED_CASE_VARIABLE,
37 Severity::Info,
38 "mixed-case-variable",
39 "mutable variables should use mixedCase"
40);
41
42impl<'ast> EarlyLintPass<'ast> for MixedCaseVariable {
43 fn check_variable_definition(
44 &mut self,
45 ctx: &LintContext,
46 var: &'ast VariableDefinition<'ast>,
47 ) {
48 if var.mutability.is_none()
49 && let Some(name) = var.name
50 && let Some(expected) =
51 check_mixed_case(name.as_str(), false, ctx.config.mixed_case_exceptions)
52 {
53 ctx.emit_with_suggestion(
54 &MIXED_CASE_VARIABLE,
55 name.span,
56 Suggestion::fix(
57 expected,
58 solar::interface::diagnostics::Applicability::MachineApplicable,
59 )
60 .with_desc("consider using"),
61 );
62 }
63 }
64}
65
66fn check_mixed_case(s: &str, is_fn: bool, allowed_patterns: &[String]) -> Option<String> {
74 if s.len() <= 1 {
75 return None;
76 }
77
78 if is_fn
80 && (s.starts_with("test") || s.starts_with("invariant_") || s.starts_with("statefulFuzz"))
81 {
82 return None;
83 }
84
85 for pattern in allowed_patterns {
87 if let Some(pos) = s.find(pattern.as_str()) {
88 let (pre, post) = s.split_at(pos);
89 let post = &post[pattern.len()..];
90
91 let is_pre_valid = pre == heck::AsLowerCamelCase(pre).to_string();
93
94 let post_trimmed = post.trim_start_matches(|c: char| c.is_numeric());
96 let is_post_valid = post_trimmed == heck::AsUpperCamelCase(post_trimmed).to_string();
97
98 if is_pre_valid && is_post_valid {
99 return None;
100 }
101 }
102 }
103
104 let suggestion = format!(
106 "{prefix}{name}{suffix}",
107 prefix = if s.starts_with('_') { "_" } else { "" },
108 name = heck::AsLowerCamelCase(s),
109 suffix = if s.ends_with('_') { "_" } else { "" }
110 );
111
112 if s == suggestion { None } else { Some(suggestion) }
114}
115
116fn is_constant_getter(header: &FunctionHeader<'_>) -> bool {
123 header.visibility().is_some_and(|v| matches!(v, Visibility::External))
124 && header.state_mutability().is_view()
125 && header.parameters.is_empty()
126 && header.returns().len() == 1
127 && header
128 .returns()
129 .first()
130 .is_some_and(|ret| ret.ty.kind.is_elementary() || ret.ty.kind.is_custom())
131 && check_screaming_snake_case(header.name.unwrap().as_str()).is_none()
132}