forge_lint/sol/info/
mixed_case.rs1use crate::{
2 linter::{EarlyLintPass, LintContext, Suggestion},
3 sol::{
4 Severity, SolLint,
5 naming::{check_mixed_case as check_mixed_case_pure, check_screaming_snake_case},
6 },
7};
8use foundry_config::lint::LintSpecificConfig;
9use solar::ast::{FunctionHeader, ItemFunction, VariableDefinition, Visibility};
10use std::sync::Arc;
11
12declare_forge_lint!(
13 MIXED_CASE_FUNCTION,
14 Severity::Info,
15 "mixed-case-function",
16 "function names should use mixedCase"
17);
18
19#[derive(Debug)]
20pub(super) struct MixedCaseFunctionPass {
21 config: Arc<LintSpecificConfig>,
22}
23
24impl MixedCaseFunctionPass {
25 pub(super) const fn new(config: Arc<LintSpecificConfig>) -> Self {
26 Self { config }
27 }
28}
29
30impl<'ast> EarlyLintPass<'ast> for MixedCaseFunctionPass {
31 fn check_item_function(&mut self, ctx: &LintContext, func: &'ast ItemFunction<'ast>) {
32 if let Some(name) = func.header.name
33 && let Some(expected) =
34 check_mixed_case(name.as_str(), true, &self.config.mixed_case_exceptions)
35 && !is_constant_getter(&func.header)
36 {
37 ctx.emit_with_suggestion(
38 &MIXED_CASE_FUNCTION,
39 name.span,
40 Suggestion::fix(
41 expected,
42 solar::interface::diagnostics::Applicability::MachineApplicable,
43 )
44 .with_desc("consider using"),
45 );
46 }
47 }
48}
49
50declare_forge_lint!(
51 MIXED_CASE_VARIABLE,
52 Severity::Info,
53 "mixed-case-variable",
54 "mutable variables should use mixedCase"
55);
56
57#[derive(Debug)]
58pub(super) struct MixedCaseVariablePass {
59 config: Arc<LintSpecificConfig>,
60}
61
62impl MixedCaseVariablePass {
63 pub(super) const fn new(config: Arc<LintSpecificConfig>) -> Self {
64 Self { config }
65 }
66}
67
68impl<'ast> EarlyLintPass<'ast> for MixedCaseVariablePass {
69 fn check_variable_definition(
70 &mut self,
71 ctx: &LintContext,
72 var: &'ast VariableDefinition<'ast>,
73 ) {
74 if var.mutability.is_none()
75 && let Some(name) = var.name
76 && let Some(expected) =
77 check_mixed_case(name.as_str(), false, &self.config.mixed_case_exceptions)
78 {
79 ctx.emit_with_suggestion(
80 &MIXED_CASE_VARIABLE,
81 name.span,
82 Suggestion::fix(
83 expected,
84 solar::interface::diagnostics::Applicability::MachineApplicable,
85 )
86 .with_desc("consider using"),
87 );
88 }
89 }
90}
91
92fn check_mixed_case(s: &str, is_fn: bool, allowed_patterns: &[String]) -> Option<String> {
95 if s.len() <= 1 {
96 return None;
97 }
98
99 if is_fn
101 && (s.starts_with("test") || s.starts_with("invariant_") || s.starts_with("statefulFuzz"))
102 {
103 return None;
104 }
105
106 for pattern in allowed_patterns {
108 if let Some(pos) = s.find(pattern.as_str()) {
109 let (pre, post) = s.split_at(pos);
110 let post = &post[pattern.len()..];
111
112 let is_pre_valid = pre == heck::AsLowerCamelCase(pre).to_string();
114
115 let post_trimmed = post.trim_start_matches(|c: char| c.is_numeric());
117 let is_post_valid = post_trimmed == heck::AsUpperCamelCase(post_trimmed).to_string();
118
119 if is_pre_valid && is_post_valid {
120 return None;
121 }
122 }
123 }
124
125 check_mixed_case_pure(s)
126}
127
128fn is_constant_getter(header: &FunctionHeader<'_>) -> bool {
135 header.visibility().is_some_and(|v| matches!(v, Visibility::External))
136 && header.state_mutability().is_view()
137 && header.parameters.is_empty()
138 && header.returns().len() == 1
139 && header
140 .returns()
141 .first()
142 .is_some_and(|ret| ret.ty.kind.is_elementary() || ret.ty.kind.is_custom())
143 && check_screaming_snake_case(header.name.unwrap().as_str()).is_none()
144}