From 559cc970732d80e3ec624c20da4f8aac219d6b2e Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Thu, 8 Oct 2020 08:33:35 +0300 Subject: Add to_upper_snake_case function to stdx --- crates/hir_ty/src/diagnostics/decl_check.rs | 4 +- .../hir_ty/src/diagnostics/decl_check/case_conv.rs | 194 ++++++++++++++++++++ .../src/diagnostics/decl_check/str_helpers.rs | 195 --------------------- crates/stdx/src/lib.rs | 12 +- 4 files changed, 206 insertions(+), 199 deletions(-) create mode 100644 crates/hir_ty/src/diagnostics/decl_check/case_conv.rs delete mode 100644 crates/hir_ty/src/diagnostics/decl_check/str_helpers.rs diff --git a/crates/hir_ty/src/diagnostics/decl_check.rs b/crates/hir_ty/src/diagnostics/decl_check.rs index 4c20921e5..901ccc94f 100644 --- a/crates/hir_ty/src/diagnostics/decl_check.rs +++ b/crates/hir_ty/src/diagnostics/decl_check.rs @@ -10,7 +10,7 @@ //! - static items (e.g. `static FOO: u8 = 10;`) //! - match arm bindings (e.g. `foo @ Some(_)`) -mod str_helpers; +mod case_conv; use hir_def::{ adt::VariantData, @@ -29,7 +29,7 @@ use syntax::{ use crate::{ db::HirDatabase, - diagnostics::{decl_check::str_helpers::*, CaseType, IncorrectCase}, + diagnostics::{decl_check::case_conv::*, CaseType, IncorrectCase}, }; pub(super) struct DeclValidator<'a, 'b: 'a> { diff --git a/crates/hir_ty/src/diagnostics/decl_check/case_conv.rs b/crates/hir_ty/src/diagnostics/decl_check/case_conv.rs new file mode 100644 index 000000000..3800f2a6b --- /dev/null +++ b/crates/hir_ty/src/diagnostics/decl_check/case_conv.rs @@ -0,0 +1,194 @@ +//! Functions for string case manipulation, such as detecting the identifier case, +//! and converting it into appropriate form. + +#[derive(Debug)] +enum DetectedCase { + LowerCamelCase, + UpperCamelCase, + LowerSnakeCase, + UpperSnakeCase, + Unknown, +} + +fn detect_case(ident: &str) -> DetectedCase { + let trimmed_ident = ident.trim_matches('_'); + let first_lowercase = trimmed_ident.starts_with(|chr: char| chr.is_ascii_lowercase()); + let mut has_lowercase = first_lowercase; + let mut has_uppercase = false; + let mut has_underscore = false; + + for chr in trimmed_ident.chars() { + if chr == '_' { + has_underscore = true; + } else if chr.is_ascii_uppercase() { + has_uppercase = true; + } else if chr.is_ascii_lowercase() { + has_lowercase = true; + } + } + + if has_uppercase { + if !has_lowercase { + DetectedCase::UpperSnakeCase + } else if !has_underscore { + if first_lowercase { + DetectedCase::LowerCamelCase + } else { + DetectedCase::UpperCamelCase + } + } else { + // It has uppercase, it has lowercase, it has underscore. + // No assumptions here + DetectedCase::Unknown + } + } else { + DetectedCase::LowerSnakeCase + } +} + +/// Converts an identifier to an UpperCamelCase form. +/// Returns `None` if the string is already is UpperCamelCase. +pub fn to_camel_case(ident: &str) -> Option { + let detected_case = detect_case(ident); + + match detected_case { + DetectedCase::UpperCamelCase => return None, + DetectedCase::LowerCamelCase => { + let mut first_capitalized = false; + let output = ident + .chars() + .map(|chr| { + if !first_capitalized && chr.is_ascii_lowercase() { + first_capitalized = true; + chr.to_ascii_uppercase() + } else { + chr + } + }) + .collect(); + return Some(output); + } + _ => {} + } + + let mut output = String::with_capacity(ident.len()); + + let mut capital_added = false; + for chr in ident.chars() { + if chr.is_alphabetic() { + if !capital_added { + output.push(chr.to_ascii_uppercase()); + capital_added = true; + } else { + output.push(chr.to_ascii_lowercase()); + } + } else if chr == '_' { + // Skip this character and make the next one capital. + capital_added = false; + } else { + // Put the characted as-is. + output.push(chr); + } + } + + if output == ident { + // While we didn't detect the correct case at the beginning, there + // may be special cases: e.g. `A` is both valid CamelCase and UPPER_SNAKE_CASE. + None + } else { + Some(output) + } +} + +/// Converts an identifier to a lower_snake_case form. +/// Returns `None` if the string is already in lower_snake_case. +pub fn to_lower_snake_case(ident: &str) -> Option { + // First, assume that it's UPPER_SNAKE_CASE. + match detect_case(ident) { + DetectedCase::LowerSnakeCase => return None, + DetectedCase::UpperSnakeCase => { + return Some(ident.chars().map(|chr| chr.to_ascii_lowercase()).collect()) + } + _ => {} + } + + // Otherwise, assume that it's CamelCase. + let lower_snake_case = stdx::to_lower_snake_case(ident); + + if lower_snake_case == ident { + // While we didn't detect the correct case at the beginning, there + // may be special cases: e.g. `a` is both valid camelCase and snake_case. + None + } else { + Some(lower_snake_case) + } +} + +/// Converts an identifier to an UPPER_SNAKE_CASE form. +/// Returns `None` if the string is already is UPPER_SNAKE_CASE. +pub fn to_upper_snake_case(ident: &str) -> Option { + match detect_case(ident) { + DetectedCase::UpperSnakeCase => return None, + DetectedCase::LowerSnakeCase => { + return Some(ident.chars().map(|chr| chr.to_ascii_uppercase()).collect()) + } + _ => {} + } + + // Normalize the string from whatever form it's in currently, and then just make it uppercase. + let upper_snake_case = stdx::to_upper_snake_case(ident); + + if upper_snake_case == ident { + // While we didn't detect the correct case at the beginning, there + // may be special cases: e.g. `A` is both valid CamelCase and UPPER_SNAKE_CASE. + None + } else { + Some(upper_snake_case) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use expect_test::{expect, Expect}; + + fn check Option>(fun: F, input: &str, expect: Expect) { + // `None` is translated to empty string, meaning that there is nothing to fix. + let output = fun(input).unwrap_or_default(); + + expect.assert_eq(&output); + } + + #[test] + fn test_to_lower_snake_case() { + check(to_lower_snake_case, "lower_snake_case", expect![[""]]); + check(to_lower_snake_case, "UPPER_SNAKE_CASE", expect![["upper_snake_case"]]); + check(to_lower_snake_case, "Weird_Case", expect![["weird_case"]]); + check(to_lower_snake_case, "CamelCase", expect![["camel_case"]]); + check(to_lower_snake_case, "lowerCamelCase", expect![["lower_camel_case"]]); + check(to_lower_snake_case, "a", expect![[""]]); + } + + #[test] + fn test_to_camel_case() { + check(to_camel_case, "CamelCase", expect![[""]]); + check(to_camel_case, "CamelCase_", expect![[""]]); + check(to_camel_case, "_CamelCase", expect![[""]]); + check(to_camel_case, "lowerCamelCase", expect![["LowerCamelCase"]]); + check(to_camel_case, "lower_snake_case", expect![["LowerSnakeCase"]]); + check(to_camel_case, "UPPER_SNAKE_CASE", expect![["UpperSnakeCase"]]); + check(to_camel_case, "Weird_Case", expect![["WeirdCase"]]); + check(to_camel_case, "name", expect![["Name"]]); + check(to_camel_case, "A", expect![[""]]); + } + + #[test] + fn test_to_upper_snake_case() { + check(to_upper_snake_case, "UPPER_SNAKE_CASE", expect![[""]]); + check(to_upper_snake_case, "lower_snake_case", expect![["LOWER_SNAKE_CASE"]]); + check(to_upper_snake_case, "Weird_Case", expect![["WEIRD_CASE"]]); + check(to_upper_snake_case, "CamelCase", expect![["CAMEL_CASE"]]); + check(to_upper_snake_case, "lowerCamelCase", expect![["LOWER_CAMEL_CASE"]]); + check(to_upper_snake_case, "A", expect![[""]]); + } +} diff --git a/crates/hir_ty/src/diagnostics/decl_check/str_helpers.rs b/crates/hir_ty/src/diagnostics/decl_check/str_helpers.rs deleted file mode 100644 index 2e1468c4c..000000000 --- a/crates/hir_ty/src/diagnostics/decl_check/str_helpers.rs +++ /dev/null @@ -1,195 +0,0 @@ -//! Functions for string case manipulation, such as detecting the identifier case, -//! and converting it into appropriate form. - -#[derive(Debug)] -enum DetectedCase { - LowerCamelCase, - UpperCamelCase, - LowerSnakeCase, - UpperSnakeCase, - Unknown, -} - -fn detect_case(ident: &str) -> DetectedCase { - let trimmed_ident = ident.trim_matches('_'); - let first_lowercase = trimmed_ident.starts_with(|chr: char| chr.is_ascii_lowercase()); - let mut has_lowercase = first_lowercase; - let mut has_uppercase = false; - let mut has_underscore = false; - - for chr in trimmed_ident.chars() { - if chr == '_' { - has_underscore = true; - } else if chr.is_ascii_uppercase() { - has_uppercase = true; - } else if chr.is_ascii_lowercase() { - has_lowercase = true; - } - } - - if has_uppercase { - if !has_lowercase { - DetectedCase::UpperSnakeCase - } else if !has_underscore { - if first_lowercase { - DetectedCase::LowerCamelCase - } else { - DetectedCase::UpperCamelCase - } - } else { - // It has uppercase, it has lowercase, it has underscore. - // No assumptions here - DetectedCase::Unknown - } - } else { - DetectedCase::LowerSnakeCase - } -} - -/// Converts an identifier to an UpperCamelCase form. -/// Returns `None` if the string is already is UpperCamelCase. -pub fn to_camel_case(ident: &str) -> Option { - let detected_case = detect_case(ident); - - match detected_case { - DetectedCase::UpperCamelCase => return None, - DetectedCase::LowerCamelCase => { - let mut first_capitalized = false; - let output = ident - .chars() - .map(|chr| { - if !first_capitalized && chr.is_ascii_lowercase() { - first_capitalized = true; - chr.to_ascii_uppercase() - } else { - chr - } - }) - .collect(); - return Some(output); - } - _ => {} - } - - let mut output = String::with_capacity(ident.len()); - - let mut capital_added = false; - for chr in ident.chars() { - if chr.is_alphabetic() { - if !capital_added { - output.push(chr.to_ascii_uppercase()); - capital_added = true; - } else { - output.push(chr.to_ascii_lowercase()); - } - } else if chr == '_' { - // Skip this character and make the next one capital. - capital_added = false; - } else { - // Put the characted as-is. - output.push(chr); - } - } - - if output == ident { - // While we didn't detect the correct case at the beginning, there - // may be special cases: e.g. `A` is both valid CamelCase and UPPER_SNAKE_CASE. - None - } else { - Some(output) - } -} - -/// Converts an identifier to a lower_snake_case form. -/// Returns `None` if the string is already in lower_snake_case. -pub fn to_lower_snake_case(ident: &str) -> Option { - // First, assume that it's UPPER_SNAKE_CASE. - match detect_case(ident) { - DetectedCase::LowerSnakeCase => return None, - DetectedCase::UpperSnakeCase => { - return Some(ident.chars().map(|chr| chr.to_ascii_lowercase()).collect()) - } - _ => {} - } - - // Otherwise, assume that it's CamelCase. - let lower_snake_case = stdx::to_lower_snake_case(ident); - - if lower_snake_case == ident { - // While we didn't detect the correct case at the beginning, there - // may be special cases: e.g. `a` is both valid camelCase and snake_case. - None - } else { - Some(lower_snake_case) - } -} - -/// Converts an identifier to an UPPER_SNAKE_CASE form. -/// Returns `None` if the string is already is UPPER_SNAKE_CASE. -pub fn to_upper_snake_case(ident: &str) -> Option { - match detect_case(ident) { - DetectedCase::UpperSnakeCase => return None, - DetectedCase::LowerSnakeCase => { - return Some(ident.chars().map(|chr| chr.to_ascii_uppercase()).collect()) - } - _ => {} - } - - // Normalize the string from whatever form it's in currently, and then just make it uppercase. - let upper_snake_case = - stdx::to_lower_snake_case(ident).chars().map(|c| c.to_ascii_uppercase()).collect(); - - if upper_snake_case == ident { - // While we didn't detect the correct case at the beginning, there - // may be special cases: e.g. `A` is both valid CamelCase and UPPER_SNAKE_CASE. - None - } else { - Some(upper_snake_case) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use expect_test::{expect, Expect}; - - fn check Option>(fun: F, input: &str, expect: Expect) { - // `None` is translated to empty string, meaning that there is nothing to fix. - let output = fun(input).unwrap_or_default(); - - expect.assert_eq(&output); - } - - #[test] - fn test_to_lower_snake_case() { - check(to_lower_snake_case, "lower_snake_case", expect![[""]]); - check(to_lower_snake_case, "UPPER_SNAKE_CASE", expect![["upper_snake_case"]]); - check(to_lower_snake_case, "Weird_Case", expect![["weird_case"]]); - check(to_lower_snake_case, "CamelCase", expect![["camel_case"]]); - check(to_lower_snake_case, "lowerCamelCase", expect![["lower_camel_case"]]); - check(to_lower_snake_case, "a", expect![[""]]); - } - - #[test] - fn test_to_camel_case() { - check(to_camel_case, "CamelCase", expect![[""]]); - check(to_camel_case, "CamelCase_", expect![[""]]); - check(to_camel_case, "_CamelCase", expect![[""]]); - check(to_camel_case, "lowerCamelCase", expect![["LowerCamelCase"]]); - check(to_camel_case, "lower_snake_case", expect![["LowerSnakeCase"]]); - check(to_camel_case, "UPPER_SNAKE_CASE", expect![["UpperSnakeCase"]]); - check(to_camel_case, "Weird_Case", expect![["WeirdCase"]]); - check(to_camel_case, "name", expect![["Name"]]); - check(to_camel_case, "A", expect![[""]]); - } - - #[test] - fn test_to_upper_snake_case() { - check(to_upper_snake_case, "UPPER_SNAKE_CASE", expect![[""]]); - check(to_upper_snake_case, "lower_snake_case", expect![["LOWER_SNAKE_CASE"]]); - check(to_upper_snake_case, "Weird_Case", expect![["WEIRD_CASE"]]); - check(to_upper_snake_case, "CamelCase", expect![["CAMEL_CASE"]]); - check(to_upper_snake_case, "lowerCamelCase", expect![["LOWER_CAMEL_CASE"]]); - check(to_upper_snake_case, "A", expect![[""]]); - } -} diff --git a/crates/stdx/src/lib.rs b/crates/stdx/src/lib.rs index b55de813e..59d89f47d 100644 --- a/crates/stdx/src/lib.rs +++ b/crates/stdx/src/lib.rs @@ -28,7 +28,7 @@ pub fn timeit(label: &'static str) -> impl Drop { Guard { label, start: Instant::now() } } -pub fn to_lower_snake_case(s: &str) -> String { +fn to_snake_case char>(s: &str, change_case: F) -> String { let mut buf = String::with_capacity(s.len()); let mut prev = false; for c in s.chars() { @@ -41,11 +41,19 @@ pub fn to_lower_snake_case(s: &str) -> String { } prev = true; - buf.push(c.to_ascii_lowercase()); + buf.push(change_case(&c)); } buf } +pub fn to_lower_snake_case(s: &str) -> String { + to_snake_case(s, char::to_ascii_lowercase) +} + +pub fn to_upper_snake_case(s: &str) -> String { + to_snake_case(s, char::to_ascii_uppercase) +} + pub fn replace(buf: &mut String, from: char, to: &str) { if !buf.contains(from) { return; -- cgit v1.2.3