Skip to main content

forge_lint/sol/info/
mixed_case.rs

1use super::{MixedCaseFunction, MixedCaseVariable};
2use crate::{
3    linter::{EarlyLintPass, LintContext, Suggestion},
4    sol::{
5        Severity, SolLint,
6        naming::{check_mixed_case as check_mixed_case_pure, check_screaming_snake_case},
7    },
8};
9use solar::ast::{FunctionHeader, ItemFunction, VariableDefinition, Visibility};
10
11declare_forge_lint!(
12    MIXED_CASE_FUNCTION,
13    Severity::Info,
14    "mixed-case-function",
15    "function names should use mixedCase"
16);
17
18impl<'ast> EarlyLintPass<'ast> for MixedCaseFunction {
19    fn check_item_function(&mut self, ctx: &LintContext, func: &'ast ItemFunction<'ast>) {
20        if let Some(name) = func.header.name
21            && let Some(expected) = check_mixed_case(
22                name.as_str(),
23                true,
24                &ctx.config.lint_specific.mixed_case_exceptions,
25            )
26            && !is_constant_getter(&func.header)
27        {
28            ctx.emit_with_suggestion(
29                &MIXED_CASE_FUNCTION,
30                name.span,
31                Suggestion::fix(
32                    expected,
33                    solar::interface::diagnostics::Applicability::MachineApplicable,
34                )
35                .with_desc("consider using"),
36            );
37        }
38    }
39}
40
41declare_forge_lint!(
42    MIXED_CASE_VARIABLE,
43    Severity::Info,
44    "mixed-case-variable",
45    "mutable variables should use mixedCase"
46);
47
48impl<'ast> EarlyLintPass<'ast> for MixedCaseVariable {
49    fn check_variable_definition(
50        &mut self,
51        ctx: &LintContext,
52        var: &'ast VariableDefinition<'ast>,
53    ) {
54        if var.mutability.is_none()
55            && let Some(name) = var.name
56            && let Some(expected) = check_mixed_case(
57                name.as_str(),
58                false,
59                &ctx.config.lint_specific.mixed_case_exceptions,
60            )
61        {
62            ctx.emit_with_suggestion(
63                &MIXED_CASE_VARIABLE,
64                name.span,
65                Suggestion::fix(
66                    expected,
67                    solar::interface::diagnostics::Applicability::MachineApplicable,
68                )
69                .with_desc("consider using"),
70            );
71        }
72    }
73}
74
75/// Wraps [`check_mixed_case_pure`] with two domain exceptions:
76/// foundry test-function prefixes and user-defined infix patterns.
77fn check_mixed_case(s: &str, is_fn: bool, allowed_patterns: &[String]) -> Option<String> {
78    if s.len() <= 1 {
79        return None;
80    }
81
82    // Exception for test, invariant, and stateful fuzzing functions.
83    if is_fn
84        && (s.starts_with("test") || s.starts_with("invariant_") || s.starts_with("statefulFuzz"))
85    {
86        return None;
87    }
88
89    // Exception for user-defined infix patterns.
90    for pattern in allowed_patterns {
91        if let Some(pos) = s.find(pattern.as_str()) {
92            let (pre, post) = s.split_at(pos);
93            let post = &post[pattern.len()..];
94
95            // Pre-pattern must be valid lowerCamelCase.
96            let is_pre_valid = pre == heck::AsLowerCamelCase(pre).to_string();
97
98            // Post-pattern must be valid UpperCamelCase (allowing leading numbers).
99            let post_trimmed = post.trim_start_matches(|c: char| c.is_numeric());
100            let is_post_valid = post_trimmed == heck::AsUpperCamelCase(post_trimmed).to_string();
101
102            if is_pre_valid && is_post_valid {
103                return None;
104            }
105        }
106    }
107
108    check_mixed_case_pure(s)
109}
110
111/// Checks if a function getter is a valid constant getter with a heuristic:
112///  * name is `SCREAMING_SNAKE_CASE`
113///  * external view visibility and mutability.
114///  * zero parameters.
115///  * exactly one return value.
116///  * return value is an elementary or a custom type
117fn is_constant_getter(header: &FunctionHeader<'_>) -> bool {
118    header.visibility().is_some_and(|v| matches!(v, Visibility::External))
119        && header.state_mutability().is_view()
120        && header.parameters.is_empty()
121        && header.returns().len() == 1
122        && header
123            .returns()
124            .first()
125            .is_some_and(|ret| ret.ty.kind.is_elementary() || ret.ty.kind.is_custom())
126        && check_screaming_snake_case(header.name.unwrap().as_str()).is_none()
127}