forge_lint/sol/info/
mixed_case.rs1use crate::{
2 linter::{EarlyLintPass, LintContext},
3 sol::{
4 Severity, SolLint,
5 naming::{
6 check_mixed_case as check_mixed_case_pure, check_screaming_snake_case, emit_rename,
7 has_acronym_exception,
8 },
9 },
10};
11use foundry_config::lint::LintSpecificConfig;
12use solar::ast::{FunctionHeader, ItemFunction, VariableDefinition, Visibility};
13use std::sync::Arc;
14
15declare_forge_lint!(
16 MIXED_CASE_FUNCTION,
17 Severity::Info,
18 "mixed-case-function",
19 "function name is not `mixedCase`"
20);
21
22declare_forge_lint!(
23 MIXED_CASE_VARIABLE,
24 Severity::Info,
25 "mixed-case-variable",
26 "mutable variable name is not `mixedCase`"
27);
28
29#[derive(Debug)]
31pub(super) struct MixedCasePass<const FUNCTIONS: bool> {
32 config: Arc<LintSpecificConfig>,
33}
34
35pub(super) type MixedCaseFunctionPass = MixedCasePass<true>;
36pub(super) type MixedCaseVariablePass = MixedCasePass<false>;
37
38impl<const FUNCTIONS: bool> MixedCasePass<FUNCTIONS> {
39 pub(super) const fn new(config: Arc<LintSpecificConfig>) -> Self {
40 Self { config }
41 }
42}
43
44impl<'ast, const FUNCTIONS: bool> EarlyLintPass<'ast> for MixedCasePass<FUNCTIONS> {
45 fn check_item_function(&mut self, ctx: &LintContext, func: &'ast ItemFunction<'ast>) {
46 if FUNCTIONS
47 && let Some(name) = func.header.name
48 && let Some(expected) =
49 check_mixed_case(name.as_str(), true, &self.config.mixed_case_exceptions)
50 && !is_constant_getter(&func.header)
51 {
52 emit_rename(ctx, &MIXED_CASE_FUNCTION, name.span, expected);
53 }
54 }
55
56 fn check_variable_definition(
57 &mut self,
58 ctx: &LintContext,
59 var: &'ast VariableDefinition<'ast>,
60 ) {
61 if !FUNCTIONS
62 && var.mutability.is_none()
63 && let Some(name) = var.name
64 && let Some(expected) =
65 check_mixed_case(name.as_str(), false, &self.config.mixed_case_exceptions)
66 {
67 emit_rename(ctx, &MIXED_CASE_VARIABLE, name.span, expected);
68 }
69 }
70}
71
72fn check_mixed_case(s: &str, is_fn: bool, allowed_patterns: &[String]) -> Option<String> {
76 if is_fn && ["test", "invariant_", "statefulFuzz"].iter().any(|prefix| s.starts_with(prefix)) {
77 return None;
78 }
79 if has_acronym_exception(s, allowed_patterns, |pre| {
80 pre == heck::AsLowerCamelCase(pre).to_string()
81 }) {
82 return None;
83 }
84 check_mixed_case_pure(s)
85}
86
87fn is_constant_getter(header: &FunctionHeader<'_>) -> bool {
90 matches!(header.visibility(), Some(Visibility::Public | Visibility::External))
91 && header.state_mutability().is_view()
92 && header.parameters.is_empty()
93 && matches!(header.returns(), [ret] if ret.ty.kind.is_elementary() || ret.ty.kind.is_custom())
94 && header.name.is_some_and(|name| check_screaming_snake_case(name.as_str()).is_none())
95}