Skip to main content

forge_lint/sol/
naming.rs

1//! Naming-convention helpers shared by Solidity lints.
2//!
3//! Each `check_*` returns `Some(suggestion)` when `s` violates the convention,
4//! `None` when it already matches. Leading/trailing underscores are preserved.
5
6use crate::{
7    linter::{LintContext, Suggestion},
8    sol::SolLint,
9};
10use solar::interface::{Span, diagnostics::Applicability};
11
12/// `Some(suggestion)` if `s` is not `PascalCase`.
13pub fn check_pascal_case(s: &str) -> Option<String> {
14    suggest(s, preserve_underscores(s, heck::AsPascalCase(s).to_string()))
15}
16
17/// `Some(suggestion)` if `s` is not `SCREAMING_SNAKE_CASE`.
18pub fn check_screaming_snake_case(s: &str) -> Option<String> {
19    suggest(s, preserve_underscores(s, heck::AsShoutySnakeCase(s).to_string()))
20}
21
22/// `Some(suggestion)` if `s` is not `mixedCase`. Pure check — domain
23/// exceptions (test-prefixes, allowed patterns, ...) live in the lint.
24pub fn check_mixed_case(s: &str) -> Option<String> {
25    suggest(s, preserve_underscores(s, heck::AsLowerCamelCase(s).to_string()))
26}
27
28/// True if `s` is `<pre><pattern><digits><post>` for one of the configured acronym `patterns`
29/// (e.g. `ERC` in `rescueERC20Tokens`), where `pre` satisfies `pre_is_valid` and `post` is
30/// `UpperCamelCase`. One preserved leading or trailing underscore is tolerated.
31pub fn has_acronym_exception(s: &str, patterns: &[String], pre_is_valid: fn(&str) -> bool) -> bool {
32    patterns.iter().any(|pattern| {
33        let Some((pre, post)) = s.split_once(pattern.as_str()) else { return false };
34        let pre = pre.strip_prefix('_').unwrap_or(pre);
35        let post = post.trim_start_matches(|c: char| c.is_numeric());
36        let post = post.strip_suffix('_').unwrap_or(post);
37        pre_is_valid(pre) && post == heck::AsUpperCamelCase(post).to_string()
38    })
39}
40
41/// Emits `lint` at `span` with a machine-applicable rename to `expected`.
42pub fn emit_rename(ctx: &LintContext, lint: &'static SolLint, span: Span, expected: String) {
43    let suggestion =
44        Suggestion::fix(expected, Applicability::MachineApplicable).with_desc("consider using");
45    ctx.emit_with_suggestion(lint, span, suggestion);
46}
47
48/// Single-character names are exempt from every convention.
49fn suggest(s: &str, expected: String) -> Option<String> {
50    (s.len() > 1 && s != expected).then_some(expected)
51}
52
53fn preserve_underscores(s: &str, body: String) -> String {
54    let prefix = if s.starts_with('_') { "_" } else { "" };
55    let suffix = if s.ends_with('_') { "_" } else { "" };
56    format!("{prefix}{body}{suffix}")
57}