forge_lint/sol/info/
mixed_case.rs1use super::{MixedCaseFunction, MixedCaseVariable};
2use crate::{
3 linter::{EarlyLintPass, LintContext, Suggestion},
4 sol::{Severity, SolLint, info::screaming_snake_case::check_screaming_snake_case},
5};
6use solar::ast::{FunctionHeader, ItemFunction, VariableDefinition, Visibility};
7
8declare_forge_lint!(
9 MIXED_CASE_FUNCTION,
10 Severity::Info,
11 "mixed-case-function",
12 "function names should use mixedCase"
13);
14
15impl<'ast> EarlyLintPass<'ast> for MixedCaseFunction {
16 fn check_item_function(&mut self, ctx: &LintContext, func: &'ast ItemFunction<'ast>) {
17 if let Some(name) = func.header.name
18 && let Some(expected) = check_mixed_case(
19 name.as_str(),
20 true,
21 &ctx.config.lint_specific.mixed_case_exceptions,
22 )
23 && !is_constant_getter(&func.header)
24 {
25 ctx.emit_with_suggestion(
26 &MIXED_CASE_FUNCTION,
27 name.span,
28 Suggestion::fix(
29 expected,
30 solar::interface::diagnostics::Applicability::MachineApplicable,
31 )
32 .with_desc("consider using"),
33 );
34 }
35 }
36}
37
38declare_forge_lint!(
39 MIXED_CASE_VARIABLE,
40 Severity::Info,
41 "mixed-case-variable",
42 "mutable variables should use mixedCase"
43);
44
45impl<'ast> EarlyLintPass<'ast> for MixedCaseVariable {
46 fn check_variable_definition(
47 &mut self,
48 ctx: &LintContext,
49 var: &'ast VariableDefinition<'ast>,
50 ) {
51 if var.mutability.is_none()
52 && let Some(name) = var.name
53 && let Some(expected) = check_mixed_case(
54 name.as_str(),
55 false,
56 &ctx.config.lint_specific.mixed_case_exceptions,
57 )
58 {
59 ctx.emit_with_suggestion(
60 &MIXED_CASE_VARIABLE,
61 name.span,
62 Suggestion::fix(
63 expected,
64 solar::interface::diagnostics::Applicability::MachineApplicable,
65 )
66 .with_desc("consider using"),
67 );
68 }
69 }
70}
71
72fn check_mixed_case(s: &str, is_fn: bool, allowed_patterns: &[String]) -> Option<String> {
80 if s.len() <= 1 {
81 return None;
82 }
83
84 if is_fn
86 && (s.starts_with("test") || s.starts_with("invariant_") || s.starts_with("statefulFuzz"))
87 {
88 return None;
89 }
90
91 for pattern in allowed_patterns {
93 if let Some(pos) = s.find(pattern.as_str()) {
94 let (pre, post) = s.split_at(pos);
95 let post = &post[pattern.len()..];
96
97 let is_pre_valid = pre == heck::AsLowerCamelCase(pre).to_string();
99
100 let post_trimmed = post.trim_start_matches(|c: char| c.is_numeric());
102 let is_post_valid = post_trimmed == heck::AsUpperCamelCase(post_trimmed).to_string();
103
104 if is_pre_valid && is_post_valid {
105 return None;
106 }
107 }
108 }
109
110 let suggestion = format!(
112 "{prefix}{name}{suffix}",
113 prefix = if s.starts_with('_') { "_" } else { "" },
114 name = heck::AsLowerCamelCase(s),
115 suffix = if s.ends_with('_') { "_" } else { "" }
116 );
117
118 if s == suggestion { None } else { Some(suggestion) }
120}
121
122fn is_constant_getter(header: &FunctionHeader<'_>) -> bool {
129 header.visibility().is_some_and(|v| matches!(v, Visibility::External))
130 && header.state_mutability().is_view()
131 && header.parameters.is_empty()
132 && header.returns().len() == 1
133 && header
134 .returns()
135 .first()
136 .is_some_and(|ret| ret.ty.kind.is_elementary() || ret.ty.kind.is_custom())
137 && check_screaming_snake_case(header.name.unwrap().as_str()).is_none()
138}