Skip to main content

forge_lint/sol/info/
mixed_case.rs

1use 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
92/// Wraps [`check_mixed_case_pure`] with two domain exceptions:
93/// foundry test-function prefixes and user-defined infix patterns.
94fn check_mixed_case(s: &str, is_fn: bool, allowed_patterns: &[String]) -> Option<String> {
95    if s.len() <= 1 {
96        return None;
97    }
98
99    // Exception for test, invariant, and stateful fuzzing functions.
100    if is_fn
101        && (s.starts_with("test") || s.starts_with("invariant_") || s.starts_with("statefulFuzz"))
102    {
103        return None;
104    }
105
106    // Exception for user-defined infix patterns.
107    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            // Pre-pattern must be valid lowerCamelCase.
113            let is_pre_valid = pre == heck::AsLowerCamelCase(pre).to_string();
114
115            // Post-pattern must be valid UpperCamelCase (allowing leading numbers).
116            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
128/// Checks if a function getter is a valid constant getter with a heuristic:
129///  * name is `SCREAMING_SNAKE_CASE`
130///  * external view visibility and mutability.
131///  * zero parameters.
132///  * exactly one return value.
133///  * return value is an elementary or a custom type
134fn 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}