diff options
Diffstat (limited to 'crates/hir_ty/src/diagnostics/decl_check')
-rw-r--r-- | crates/hir_ty/src/diagnostics/decl_check/case_conv.rs | 194 |
1 files changed, 194 insertions, 0 deletions
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 @@ | |||
1 | //! Functions for string case manipulation, such as detecting the identifier case, | ||
2 | //! and converting it into appropriate form. | ||
3 | |||
4 | #[derive(Debug)] | ||
5 | enum DetectedCase { | ||
6 | LowerCamelCase, | ||
7 | UpperCamelCase, | ||
8 | LowerSnakeCase, | ||
9 | UpperSnakeCase, | ||
10 | Unknown, | ||
11 | } | ||
12 | |||
13 | fn detect_case(ident: &str) -> DetectedCase { | ||
14 | let trimmed_ident = ident.trim_matches('_'); | ||
15 | let first_lowercase = trimmed_ident.starts_with(|chr: char| chr.is_ascii_lowercase()); | ||
16 | let mut has_lowercase = first_lowercase; | ||
17 | let mut has_uppercase = false; | ||
18 | let mut has_underscore = false; | ||
19 | |||
20 | for chr in trimmed_ident.chars() { | ||
21 | if chr == '_' { | ||
22 | has_underscore = true; | ||
23 | } else if chr.is_ascii_uppercase() { | ||
24 | has_uppercase = true; | ||
25 | } else if chr.is_ascii_lowercase() { | ||
26 | has_lowercase = true; | ||
27 | } | ||
28 | } | ||
29 | |||
30 | if has_uppercase { | ||
31 | if !has_lowercase { | ||
32 | DetectedCase::UpperSnakeCase | ||
33 | } else if !has_underscore { | ||
34 | if first_lowercase { | ||
35 | DetectedCase::LowerCamelCase | ||
36 | } else { | ||
37 | DetectedCase::UpperCamelCase | ||
38 | } | ||
39 | } else { | ||
40 | // It has uppercase, it has lowercase, it has underscore. | ||
41 | // No assumptions here | ||
42 | DetectedCase::Unknown | ||
43 | } | ||
44 | } else { | ||
45 | DetectedCase::LowerSnakeCase | ||
46 | } | ||
47 | } | ||
48 | |||
49 | /// Converts an identifier to an UpperCamelCase form. | ||
50 | /// Returns `None` if the string is already is UpperCamelCase. | ||
51 | pub fn to_camel_case(ident: &str) -> Option<String> { | ||
52 | let detected_case = detect_case(ident); | ||
53 | |||
54 | match detected_case { | ||
55 | DetectedCase::UpperCamelCase => return None, | ||
56 | DetectedCase::LowerCamelCase => { | ||
57 | let mut first_capitalized = false; | ||
58 | let output = ident | ||
59 | .chars() | ||
60 | .map(|chr| { | ||
61 | if !first_capitalized && chr.is_ascii_lowercase() { | ||
62 | first_capitalized = true; | ||
63 | chr.to_ascii_uppercase() | ||
64 | } else { | ||
65 | chr | ||
66 | } | ||
67 | }) | ||
68 | .collect(); | ||
69 | return Some(output); | ||
70 | } | ||
71 | _ => {} | ||
72 | } | ||
73 | |||
74 | let mut output = String::with_capacity(ident.len()); | ||
75 | |||
76 | let mut capital_added = false; | ||
77 | for chr in ident.chars() { | ||
78 | if chr.is_alphabetic() { | ||
79 | if !capital_added { | ||
80 | output.push(chr.to_ascii_uppercase()); | ||
81 | capital_added = true; | ||
82 | } else { | ||
83 | output.push(chr.to_ascii_lowercase()); | ||
84 | } | ||
85 | } else if chr == '_' { | ||
86 | // Skip this character and make the next one capital. | ||
87 | capital_added = false; | ||
88 | } else { | ||
89 | // Put the characted as-is. | ||
90 | output.push(chr); | ||
91 | } | ||
92 | } | ||
93 | |||
94 | if output == ident { | ||
95 | // While we didn't detect the correct case at the beginning, there | ||
96 | // may be special cases: e.g. `A` is both valid CamelCase and UPPER_SNAKE_CASE. | ||
97 | None | ||
98 | } else { | ||
99 | Some(output) | ||
100 | } | ||
101 | } | ||
102 | |||
103 | /// Converts an identifier to a lower_snake_case form. | ||
104 | /// Returns `None` if the string is already in lower_snake_case. | ||
105 | pub fn to_lower_snake_case(ident: &str) -> Option<String> { | ||
106 | // First, assume that it's UPPER_SNAKE_CASE. | ||
107 | match detect_case(ident) { | ||
108 | DetectedCase::LowerSnakeCase => return None, | ||
109 | DetectedCase::UpperSnakeCase => { | ||
110 | return Some(ident.chars().map(|chr| chr.to_ascii_lowercase()).collect()) | ||
111 | } | ||
112 | _ => {} | ||
113 | } | ||
114 | |||
115 | // Otherwise, assume that it's CamelCase. | ||
116 | let lower_snake_case = stdx::to_lower_snake_case(ident); | ||
117 | |||
118 | if lower_snake_case == ident { | ||
119 | // While we didn't detect the correct case at the beginning, there | ||
120 | // may be special cases: e.g. `a` is both valid camelCase and snake_case. | ||
121 | None | ||
122 | } else { | ||
123 | Some(lower_snake_case) | ||
124 | } | ||
125 | } | ||
126 | |||
127 | /// Converts an identifier to an UPPER_SNAKE_CASE form. | ||
128 | /// Returns `None` if the string is already is UPPER_SNAKE_CASE. | ||
129 | pub fn to_upper_snake_case(ident: &str) -> Option<String> { | ||
130 | match detect_case(ident) { | ||
131 | DetectedCase::UpperSnakeCase => return None, | ||
132 | DetectedCase::LowerSnakeCase => { | ||
133 | return Some(ident.chars().map(|chr| chr.to_ascii_uppercase()).collect()) | ||
134 | } | ||
135 | _ => {} | ||
136 | } | ||
137 | |||
138 | // Normalize the string from whatever form it's in currently, and then just make it uppercase. | ||
139 | let upper_snake_case = stdx::to_upper_snake_case(ident); | ||
140 | |||
141 | if upper_snake_case == ident { | ||
142 | // While we didn't detect the correct case at the beginning, there | ||
143 | // may be special cases: e.g. `A` is both valid CamelCase and UPPER_SNAKE_CASE. | ||
144 | None | ||
145 | } else { | ||
146 | Some(upper_snake_case) | ||
147 | } | ||
148 | } | ||
149 | |||
150 | #[cfg(test)] | ||
151 | mod tests { | ||
152 | use super::*; | ||
153 | use expect_test::{expect, Expect}; | ||
154 | |||
155 | fn check<F: Fn(&str) -> Option<String>>(fun: F, input: &str, expect: Expect) { | ||
156 | // `None` is translated to empty string, meaning that there is nothing to fix. | ||
157 | let output = fun(input).unwrap_or_default(); | ||
158 | |||
159 | expect.assert_eq(&output); | ||
160 | } | ||
161 | |||
162 | #[test] | ||
163 | fn test_to_lower_snake_case() { | ||
164 | check(to_lower_snake_case, "lower_snake_case", expect![[""]]); | ||
165 | check(to_lower_snake_case, "UPPER_SNAKE_CASE", expect![["upper_snake_case"]]); | ||
166 | check(to_lower_snake_case, "Weird_Case", expect![["weird_case"]]); | ||
167 | check(to_lower_snake_case, "CamelCase", expect![["camel_case"]]); | ||
168 | check(to_lower_snake_case, "lowerCamelCase", expect![["lower_camel_case"]]); | ||
169 | check(to_lower_snake_case, "a", expect![[""]]); | ||
170 | } | ||
171 | |||
172 | #[test] | ||
173 | fn test_to_camel_case() { | ||
174 | check(to_camel_case, "CamelCase", expect![[""]]); | ||
175 | check(to_camel_case, "CamelCase_", expect![[""]]); | ||
176 | check(to_camel_case, "_CamelCase", expect![[""]]); | ||
177 | check(to_camel_case, "lowerCamelCase", expect![["LowerCamelCase"]]); | ||
178 | check(to_camel_case, "lower_snake_case", expect![["LowerSnakeCase"]]); | ||
179 | check(to_camel_case, "UPPER_SNAKE_CASE", expect![["UpperSnakeCase"]]); | ||
180 | check(to_camel_case, "Weird_Case", expect![["WeirdCase"]]); | ||
181 | check(to_camel_case, "name", expect![["Name"]]); | ||
182 | check(to_camel_case, "A", expect![[""]]); | ||
183 | } | ||
184 | |||
185 | #[test] | ||
186 | fn test_to_upper_snake_case() { | ||
187 | check(to_upper_snake_case, "UPPER_SNAKE_CASE", expect![[""]]); | ||
188 | check(to_upper_snake_case, "lower_snake_case", expect![["LOWER_SNAKE_CASE"]]); | ||
189 | check(to_upper_snake_case, "Weird_Case", expect![["WEIRD_CASE"]]); | ||
190 | check(to_upper_snake_case, "CamelCase", expect![["CAMEL_CASE"]]); | ||
191 | check(to_upper_snake_case, "lowerCamelCase", expect![["LOWER_CAMEL_CASE"]]); | ||
192 | check(to_upper_snake_case, "A", expect![[""]]); | ||
193 | } | ||
194 | } | ||