Skip to main content

forge_lint/sol/info/
mixed_case.rs

1use 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/// Checks function names when `FUNCTIONS` is set, mutable variable names otherwise.
30#[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
72/// Wraps [`check_mixed_case_pure`] with two domain exceptions: foundry test-function prefixes
73/// and user-defined infix patterns, which split the name into a lowerCamelCase prefix and an
74/// UpperCamelCase suffix (allowing leading digits).
75fn 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
87/// Heuristic for a getter of a constant: `SCREAMING_SNAKE_CASE` name, `public view` or `external
88/// view`, no parameters and exactly one elementary or custom-typed return value.
89fn 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}