forge_lint/sol/info/
mixed_case.rs1use 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
75fn check_mixed_case(s: &str, is_fn: bool, allowed_patterns: &[String]) -> Option<String> {
78 if s.len() <= 1 {
79 return None;
80 }
81
82 if is_fn
84 && (s.starts_with("test") || s.starts_with("invariant_") || s.starts_with("statefulFuzz"))
85 {
86 return None;
87 }
88
89 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 let is_pre_valid = pre == heck::AsLowerCamelCase(pre).to_string();
97
98 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
111fn 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}