diff options
Diffstat (limited to 'crates/hir_ty')
-rw-r--r-- | crates/hir_ty/src/diagnostics.rs | 85 | ||||
-rw-r--r-- | crates/hir_ty/src/diagnostics/decl_check.rs | 833 | ||||
-rw-r--r-- | crates/hir_ty/src/diagnostics/decl_check/case_conv.rs | 194 | ||||
-rw-r--r-- | crates/hir_ty/src/diagnostics/unsafe_check.rs | 6 |
4 files changed, 1112 insertions, 6 deletions
diff --git a/crates/hir_ty/src/diagnostics.rs b/crates/hir_ty/src/diagnostics.rs index 9ba005fab..dfe98571e 100644 --- a/crates/hir_ty/src/diagnostics.rs +++ b/crates/hir_ty/src/diagnostics.rs | |||
@@ -2,10 +2,11 @@ | |||
2 | mod expr; | 2 | mod expr; |
3 | mod match_check; | 3 | mod match_check; |
4 | mod unsafe_check; | 4 | mod unsafe_check; |
5 | mod decl_check; | ||
5 | 6 | ||
6 | use std::any::Any; | 7 | use std::{any::Any, fmt}; |
7 | 8 | ||
8 | use hir_def::DefWithBodyId; | 9 | use hir_def::{DefWithBodyId, ModuleDefId}; |
9 | use hir_expand::diagnostics::{Diagnostic, DiagnosticCode, DiagnosticSink}; | 10 | use hir_expand::diagnostics::{Diagnostic, DiagnosticCode, DiagnosticSink}; |
10 | use hir_expand::{name::Name, HirFileId, InFile}; | 11 | use hir_expand::{name::Name, HirFileId, InFile}; |
11 | use stdx::format_to; | 12 | use stdx::format_to; |
@@ -15,6 +16,16 @@ use crate::db::HirDatabase; | |||
15 | 16 | ||
16 | pub use crate::diagnostics::expr::{record_literal_missing_fields, record_pattern_missing_fields}; | 17 | pub use crate::diagnostics::expr::{record_literal_missing_fields, record_pattern_missing_fields}; |
17 | 18 | ||
19 | pub fn validate_module_item( | ||
20 | db: &dyn HirDatabase, | ||
21 | owner: ModuleDefId, | ||
22 | sink: &mut DiagnosticSink<'_>, | ||
23 | ) { | ||
24 | let _p = profile::span("validate_module_item"); | ||
25 | let mut validator = decl_check::DeclValidator::new(owner, sink); | ||
26 | validator.validate_item(db); | ||
27 | } | ||
28 | |||
18 | pub fn validate_body(db: &dyn HirDatabase, owner: DefWithBodyId, sink: &mut DiagnosticSink<'_>) { | 29 | pub fn validate_body(db: &dyn HirDatabase, owner: DefWithBodyId, sink: &mut DiagnosticSink<'_>) { |
19 | let _p = profile::span("validate_body"); | 30 | let _p = profile::span("validate_body"); |
20 | let infer = db.infer(owner); | 31 | let infer = db.infer(owner); |
@@ -231,6 +242,66 @@ impl Diagnostic for MismatchedArgCount { | |||
231 | } | 242 | } |
232 | } | 243 | } |
233 | 244 | ||
245 | #[derive(Debug)] | ||
246 | pub enum CaseType { | ||
247 | // `some_var` | ||
248 | LowerSnakeCase, | ||
249 | // `SOME_CONST` | ||
250 | UpperSnakeCase, | ||
251 | // `SomeStruct` | ||
252 | UpperCamelCase, | ||
253 | } | ||
254 | |||
255 | impl fmt::Display for CaseType { | ||
256 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
257 | let repr = match self { | ||
258 | CaseType::LowerSnakeCase => "snake_case", | ||
259 | CaseType::UpperSnakeCase => "UPPER_SNAKE_CASE", | ||
260 | CaseType::UpperCamelCase => "CamelCase", | ||
261 | }; | ||
262 | |||
263 | write!(f, "{}", repr) | ||
264 | } | ||
265 | } | ||
266 | |||
267 | #[derive(Debug)] | ||
268 | pub struct IncorrectCase { | ||
269 | pub file: HirFileId, | ||
270 | pub ident: AstPtr<ast::Name>, | ||
271 | pub expected_case: CaseType, | ||
272 | pub ident_type: String, | ||
273 | pub ident_text: String, | ||
274 | pub suggested_text: String, | ||
275 | } | ||
276 | |||
277 | impl Diagnostic for IncorrectCase { | ||
278 | fn code(&self) -> DiagnosticCode { | ||
279 | DiagnosticCode("incorrect-ident-case") | ||
280 | } | ||
281 | |||
282 | fn message(&self) -> String { | ||
283 | format!( | ||
284 | "{} `{}` should have {} name, e.g. `{}`", | ||
285 | self.ident_type, | ||
286 | self.ident_text, | ||
287 | self.expected_case.to_string(), | ||
288 | self.suggested_text | ||
289 | ) | ||
290 | } | ||
291 | |||
292 | fn display_source(&self) -> InFile<SyntaxNodePtr> { | ||
293 | InFile::new(self.file, self.ident.clone().into()) | ||
294 | } | ||
295 | |||
296 | fn as_any(&self) -> &(dyn Any + Send + 'static) { | ||
297 | self | ||
298 | } | ||
299 | |||
300 | fn is_experimental(&self) -> bool { | ||
301 | true | ||
302 | } | ||
303 | } | ||
304 | |||
234 | #[cfg(test)] | 305 | #[cfg(test)] |
235 | mod tests { | 306 | mod tests { |
236 | use base_db::{fixture::WithFixture, FileId, SourceDatabase, SourceDatabaseExt}; | 307 | use base_db::{fixture::WithFixture, FileId, SourceDatabase, SourceDatabaseExt}; |
@@ -242,7 +313,10 @@ mod tests { | |||
242 | use rustc_hash::FxHashMap; | 313 | use rustc_hash::FxHashMap; |
243 | use syntax::{TextRange, TextSize}; | 314 | use syntax::{TextRange, TextSize}; |
244 | 315 | ||
245 | use crate::{diagnostics::validate_body, test_db::TestDB}; | 316 | use crate::{ |
317 | diagnostics::{validate_body, validate_module_item}, | ||
318 | test_db::TestDB, | ||
319 | }; | ||
246 | 320 | ||
247 | impl TestDB { | 321 | impl TestDB { |
248 | fn diagnostics<F: FnMut(&dyn Diagnostic)>(&self, mut cb: F) { | 322 | fn diagnostics<F: FnMut(&dyn Diagnostic)>(&self, mut cb: F) { |
@@ -253,6 +327,9 @@ mod tests { | |||
253 | let mut fns = Vec::new(); | 327 | let mut fns = Vec::new(); |
254 | for (module_id, _) in crate_def_map.modules.iter() { | 328 | for (module_id, _) in crate_def_map.modules.iter() { |
255 | for decl in crate_def_map[module_id].scope.declarations() { | 329 | for decl in crate_def_map[module_id].scope.declarations() { |
330 | let mut sink = DiagnosticSinkBuilder::new().build(&mut cb); | ||
331 | validate_module_item(self, decl, &mut sink); | ||
332 | |||
256 | if let ModuleDefId::FunctionId(f) = decl { | 333 | if let ModuleDefId::FunctionId(f) = decl { |
257 | fns.push(f) | 334 | fns.push(f) |
258 | } | 335 | } |
@@ -262,6 +339,8 @@ mod tests { | |||
262 | let impl_data = self.impl_data(impl_id); | 339 | let impl_data = self.impl_data(impl_id); |
263 | for item in impl_data.items.iter() { | 340 | for item in impl_data.items.iter() { |
264 | if let AssocItemId::FunctionId(f) = item { | 341 | if let AssocItemId::FunctionId(f) = item { |
342 | let mut sink = DiagnosticSinkBuilder::new().build(&mut cb); | ||
343 | validate_module_item(self, ModuleDefId::FunctionId(*f), &mut sink); | ||
265 | fns.push(*f) | 344 | fns.push(*f) |
266 | } | 345 | } |
267 | } | 346 | } |
diff --git a/crates/hir_ty/src/diagnostics/decl_check.rs b/crates/hir_ty/src/diagnostics/decl_check.rs new file mode 100644 index 000000000..f987636fe --- /dev/null +++ b/crates/hir_ty/src/diagnostics/decl_check.rs | |||
@@ -0,0 +1,833 @@ | |||
1 | //! Provides validators for the item declarations. | ||
2 | //! | ||
3 | //! This includes the following items: | ||
4 | //! | ||
5 | //! - variable bindings (e.g. `let x = foo();`) | ||
6 | //! - struct fields (e.g. `struct Foo { field: u8 }`) | ||
7 | //! - enum variants (e.g. `enum Foo { Variant { field: u8 } }`) | ||
8 | //! - function/method arguments (e.g. `fn foo(arg: u8)`) | ||
9 | //! - constants (e.g. `const FOO: u8 = 10;`) | ||
10 | //! - static items (e.g. `static FOO: u8 = 10;`) | ||
11 | //! - match arm bindings (e.g. `foo @ Some(_)`) | ||
12 | |||
13 | mod case_conv; | ||
14 | |||
15 | use hir_def::{ | ||
16 | adt::VariantData, | ||
17 | expr::{Pat, PatId}, | ||
18 | src::HasSource, | ||
19 | AdtId, ConstId, EnumId, FunctionId, Lookup, ModuleDefId, StaticId, StructId, | ||
20 | }; | ||
21 | use hir_expand::{ | ||
22 | diagnostics::DiagnosticSink, | ||
23 | name::{AsName, Name}, | ||
24 | }; | ||
25 | use syntax::{ | ||
26 | ast::{self, NameOwner}, | ||
27 | AstNode, AstPtr, | ||
28 | }; | ||
29 | |||
30 | use crate::{ | ||
31 | db::HirDatabase, | ||
32 | diagnostics::{decl_check::case_conv::*, CaseType, IncorrectCase}, | ||
33 | }; | ||
34 | |||
35 | pub(super) struct DeclValidator<'a, 'b: 'a> { | ||
36 | owner: ModuleDefId, | ||
37 | sink: &'a mut DiagnosticSink<'b>, | ||
38 | } | ||
39 | |||
40 | #[derive(Debug)] | ||
41 | struct Replacement { | ||
42 | current_name: Name, | ||
43 | suggested_text: String, | ||
44 | expected_case: CaseType, | ||
45 | } | ||
46 | |||
47 | impl<'a, 'b> DeclValidator<'a, 'b> { | ||
48 | pub(super) fn new( | ||
49 | owner: ModuleDefId, | ||
50 | sink: &'a mut DiagnosticSink<'b>, | ||
51 | ) -> DeclValidator<'a, 'b> { | ||
52 | DeclValidator { owner, sink } | ||
53 | } | ||
54 | |||
55 | pub(super) fn validate_item(&mut self, db: &dyn HirDatabase) { | ||
56 | match self.owner { | ||
57 | ModuleDefId::FunctionId(func) => self.validate_func(db, func), | ||
58 | ModuleDefId::AdtId(adt) => self.validate_adt(db, adt), | ||
59 | ModuleDefId::ConstId(const_id) => self.validate_const(db, const_id), | ||
60 | ModuleDefId::StaticId(static_id) => self.validate_static(db, static_id), | ||
61 | _ => return, | ||
62 | } | ||
63 | } | ||
64 | |||
65 | fn validate_adt(&mut self, db: &dyn HirDatabase, adt: AdtId) { | ||
66 | match adt { | ||
67 | AdtId::StructId(struct_id) => self.validate_struct(db, struct_id), | ||
68 | AdtId::EnumId(enum_id) => self.validate_enum(db, enum_id), | ||
69 | AdtId::UnionId(_) => { | ||
70 | // Unions aren't yet supported by this validator. | ||
71 | } | ||
72 | } | ||
73 | } | ||
74 | |||
75 | fn validate_func(&mut self, db: &dyn HirDatabase, func: FunctionId) { | ||
76 | let data = db.function_data(func); | ||
77 | let body = db.body(func.into()); | ||
78 | |||
79 | // 1. Check the function name. | ||
80 | let function_name = data.name.to_string(); | ||
81 | let fn_name_replacement = if let Some(new_name) = to_lower_snake_case(&function_name) { | ||
82 | let replacement = Replacement { | ||
83 | current_name: data.name.clone(), | ||
84 | suggested_text: new_name, | ||
85 | expected_case: CaseType::LowerSnakeCase, | ||
86 | }; | ||
87 | Some(replacement) | ||
88 | } else { | ||
89 | None | ||
90 | }; | ||
91 | |||
92 | // 2. Check the param names. | ||
93 | let mut fn_param_replacements = Vec::new(); | ||
94 | |||
95 | for pat_id in body.params.iter().cloned() { | ||
96 | let pat = &body[pat_id]; | ||
97 | |||
98 | let param_name = match pat { | ||
99 | Pat::Bind { name, .. } => name, | ||
100 | _ => continue, | ||
101 | }; | ||
102 | |||
103 | let name = param_name.to_string(); | ||
104 | if let Some(new_name) = to_lower_snake_case(&name) { | ||
105 | let replacement = Replacement { | ||
106 | current_name: param_name.clone(), | ||
107 | suggested_text: new_name, | ||
108 | expected_case: CaseType::LowerSnakeCase, | ||
109 | }; | ||
110 | fn_param_replacements.push(replacement); | ||
111 | } | ||
112 | } | ||
113 | |||
114 | // 3. Check the patterns inside the function body. | ||
115 | let mut pats_replacements = Vec::new(); | ||
116 | |||
117 | for (pat_idx, pat) in body.pats.iter() { | ||
118 | if body.params.contains(&pat_idx) { | ||
119 | // We aren't interested in function parameters, we've processed them above. | ||
120 | continue; | ||
121 | } | ||
122 | |||
123 | let bind_name = match pat { | ||
124 | Pat::Bind { name, .. } => name, | ||
125 | _ => continue, | ||
126 | }; | ||
127 | |||
128 | let name = bind_name.to_string(); | ||
129 | if let Some(new_name) = to_lower_snake_case(&name) { | ||
130 | let replacement = Replacement { | ||
131 | current_name: bind_name.clone(), | ||
132 | suggested_text: new_name, | ||
133 | expected_case: CaseType::LowerSnakeCase, | ||
134 | }; | ||
135 | pats_replacements.push((pat_idx, replacement)); | ||
136 | } | ||
137 | } | ||
138 | |||
139 | // 4. If there is at least one element to spawn a warning on, go to the source map and generate a warning. | ||
140 | self.create_incorrect_case_diagnostic_for_func( | ||
141 | func, | ||
142 | db, | ||
143 | fn_name_replacement, | ||
144 | fn_param_replacements, | ||
145 | ); | ||
146 | self.create_incorrect_case_diagnostic_for_variables(func, db, pats_replacements); | ||
147 | |||
148 | // 5. Recursively validate inner scope items, such as static variables and constants. | ||
149 | for (item_id, _) in body.item_scope.values() { | ||
150 | let mut validator = DeclValidator::new(item_id, self.sink); | ||
151 | validator.validate_item(db); | ||
152 | } | ||
153 | } | ||
154 | |||
155 | /// Given the information about incorrect names in the function declaration, looks up into the source code | ||
156 | /// for exact locations and adds diagnostics into the sink. | ||
157 | fn create_incorrect_case_diagnostic_for_func( | ||
158 | &mut self, | ||
159 | func: FunctionId, | ||
160 | db: &dyn HirDatabase, | ||
161 | fn_name_replacement: Option<Replacement>, | ||
162 | fn_param_replacements: Vec<Replacement>, | ||
163 | ) { | ||
164 | // XXX: only look at sources if we do have incorrect names | ||
165 | if fn_name_replacement.is_none() && fn_param_replacements.is_empty() { | ||
166 | return; | ||
167 | } | ||
168 | |||
169 | let fn_loc = func.lookup(db.upcast()); | ||
170 | let fn_src = fn_loc.source(db.upcast()); | ||
171 | |||
172 | // 1. Diagnostic for function name. | ||
173 | if let Some(replacement) = fn_name_replacement { | ||
174 | let ast_ptr = match fn_src.value.name() { | ||
175 | Some(name) => name, | ||
176 | None => { | ||
177 | // We don't want rust-analyzer to panic over this, but it is definitely some kind of error in the logic. | ||
178 | log::error!( | ||
179 | "Replacement ({:?}) was generated for a function without a name: {:?}", | ||
180 | replacement, | ||
181 | fn_src | ||
182 | ); | ||
183 | return; | ||
184 | } | ||
185 | }; | ||
186 | |||
187 | let diagnostic = IncorrectCase { | ||
188 | file: fn_src.file_id, | ||
189 | ident_type: "Function".to_string(), | ||
190 | ident: AstPtr::new(&ast_ptr).into(), | ||
191 | expected_case: replacement.expected_case, | ||
192 | ident_text: replacement.current_name.to_string(), | ||
193 | suggested_text: replacement.suggested_text, | ||
194 | }; | ||
195 | |||
196 | self.sink.push(diagnostic); | ||
197 | } | ||
198 | |||
199 | // 2. Diagnostics for function params. | ||
200 | let fn_params_list = match fn_src.value.param_list() { | ||
201 | Some(params) => params, | ||
202 | None => { | ||
203 | if !fn_param_replacements.is_empty() { | ||
204 | log::error!( | ||
205 | "Replacements ({:?}) were generated for a function parameters which had no parameters list: {:?}", | ||
206 | fn_param_replacements, fn_src | ||
207 | ); | ||
208 | } | ||
209 | return; | ||
210 | } | ||
211 | }; | ||
212 | let mut fn_params_iter = fn_params_list.params(); | ||
213 | for param_to_rename in fn_param_replacements { | ||
214 | // We assume that parameters in replacement are in the same order as in the | ||
215 | // actual params list, but just some of them (ones that named correctly) are skipped. | ||
216 | let ast_ptr: ast::Name = loop { | ||
217 | match fn_params_iter.next() { | ||
218 | Some(element) | ||
219 | if pat_equals_to_name(element.pat(), ¶m_to_rename.current_name) => | ||
220 | { | ||
221 | if let ast::Pat::IdentPat(pat) = element.pat().unwrap() { | ||
222 | break pat.name().unwrap(); | ||
223 | } else { | ||
224 | // This is critical. If we consider this parameter the expected one, | ||
225 | // it **must** have a name. | ||
226 | panic!( | ||
227 | "Pattern {:?} equals to expected replacement {:?}, but has no name", | ||
228 | element, param_to_rename | ||
229 | ); | ||
230 | } | ||
231 | } | ||
232 | Some(_) => {} | ||
233 | None => { | ||
234 | log::error!( | ||
235 | "Replacement ({:?}) was generated for a function parameter which was not found: {:?}", | ||
236 | param_to_rename, fn_src | ||
237 | ); | ||
238 | return; | ||
239 | } | ||
240 | } | ||
241 | }; | ||
242 | |||
243 | let diagnostic = IncorrectCase { | ||
244 | file: fn_src.file_id, | ||
245 | ident_type: "Argument".to_string(), | ||
246 | ident: AstPtr::new(&ast_ptr).into(), | ||
247 | expected_case: param_to_rename.expected_case, | ||
248 | ident_text: param_to_rename.current_name.to_string(), | ||
249 | suggested_text: param_to_rename.suggested_text, | ||
250 | }; | ||
251 | |||
252 | self.sink.push(diagnostic); | ||
253 | } | ||
254 | } | ||
255 | |||
256 | /// Given the information about incorrect variable names, looks up into the source code | ||
257 | /// for exact locations and adds diagnostics into the sink. | ||
258 | fn create_incorrect_case_diagnostic_for_variables( | ||
259 | &mut self, | ||
260 | func: FunctionId, | ||
261 | db: &dyn HirDatabase, | ||
262 | pats_replacements: Vec<(PatId, Replacement)>, | ||
263 | ) { | ||
264 | // XXX: only look at source_map if we do have missing fields | ||
265 | if pats_replacements.is_empty() { | ||
266 | return; | ||
267 | } | ||
268 | |||
269 | let (_, source_map) = db.body_with_source_map(func.into()); | ||
270 | |||
271 | for (id, replacement) in pats_replacements { | ||
272 | if let Ok(source_ptr) = source_map.pat_syntax(id) { | ||
273 | if let Some(expr) = source_ptr.value.as_ref().left() { | ||
274 | let root = source_ptr.file_syntax(db.upcast()); | ||
275 | if let ast::Pat::IdentPat(ident_pat) = expr.to_node(&root) { | ||
276 | let parent = match ident_pat.syntax().parent() { | ||
277 | Some(parent) => parent, | ||
278 | None => continue, | ||
279 | }; | ||
280 | let name_ast = match ident_pat.name() { | ||
281 | Some(name_ast) => name_ast, | ||
282 | None => continue, | ||
283 | }; | ||
284 | |||
285 | // We have to check that it's either `let var = ...` or `var @ Variant(_)` statement, | ||
286 | // because e.g. match arms are patterns as well. | ||
287 | // In other words, we check that it's a named variable binding. | ||
288 | let is_binding = ast::LetStmt::cast(parent.clone()).is_some() | ||
289 | || (ast::MatchArm::cast(parent).is_some() | ||
290 | && ident_pat.at_token().is_some()); | ||
291 | if !is_binding { | ||
292 | // This pattern is not an actual variable declaration, e.g. `Some(val) => {..}` match arm. | ||
293 | continue; | ||
294 | } | ||
295 | |||
296 | let diagnostic = IncorrectCase { | ||
297 | file: source_ptr.file_id, | ||
298 | ident_type: "Variable".to_string(), | ||
299 | ident: AstPtr::new(&name_ast).into(), | ||
300 | expected_case: replacement.expected_case, | ||
301 | ident_text: replacement.current_name.to_string(), | ||
302 | suggested_text: replacement.suggested_text, | ||
303 | }; | ||
304 | |||
305 | self.sink.push(diagnostic); | ||
306 | } | ||
307 | } | ||
308 | } | ||
309 | } | ||
310 | } | ||
311 | |||
312 | fn validate_struct(&mut self, db: &dyn HirDatabase, struct_id: StructId) { | ||
313 | let data = db.struct_data(struct_id); | ||
314 | |||
315 | // 1. Check the structure name. | ||
316 | let struct_name = data.name.to_string(); | ||
317 | let struct_name_replacement = if let Some(new_name) = to_camel_case(&struct_name) { | ||
318 | let replacement = Replacement { | ||
319 | current_name: data.name.clone(), | ||
320 | suggested_text: new_name, | ||
321 | expected_case: CaseType::UpperCamelCase, | ||
322 | }; | ||
323 | Some(replacement) | ||
324 | } else { | ||
325 | None | ||
326 | }; | ||
327 | |||
328 | // 2. Check the field names. | ||
329 | let mut struct_fields_replacements = Vec::new(); | ||
330 | |||
331 | if let VariantData::Record(fields) = data.variant_data.as_ref() { | ||
332 | for (_, field) in fields.iter() { | ||
333 | let field_name = field.name.to_string(); | ||
334 | if let Some(new_name) = to_lower_snake_case(&field_name) { | ||
335 | let replacement = Replacement { | ||
336 | current_name: field.name.clone(), | ||
337 | suggested_text: new_name, | ||
338 | expected_case: CaseType::LowerSnakeCase, | ||
339 | }; | ||
340 | struct_fields_replacements.push(replacement); | ||
341 | } | ||
342 | } | ||
343 | } | ||
344 | |||
345 | // 3. If there is at least one element to spawn a warning on, go to the source map and generate a warning. | ||
346 | self.create_incorrect_case_diagnostic_for_struct( | ||
347 | struct_id, | ||
348 | db, | ||
349 | struct_name_replacement, | ||
350 | struct_fields_replacements, | ||
351 | ); | ||
352 | } | ||
353 | |||
354 | /// Given the information about incorrect names in the struct declaration, looks up into the source code | ||
355 | /// for exact locations and adds diagnostics into the sink. | ||
356 | fn create_incorrect_case_diagnostic_for_struct( | ||
357 | &mut self, | ||
358 | struct_id: StructId, | ||
359 | db: &dyn HirDatabase, | ||
360 | struct_name_replacement: Option<Replacement>, | ||
361 | struct_fields_replacements: Vec<Replacement>, | ||
362 | ) { | ||
363 | // XXX: only look at sources if we do have incorrect names | ||
364 | if struct_name_replacement.is_none() && struct_fields_replacements.is_empty() { | ||
365 | return; | ||
366 | } | ||
367 | |||
368 | let struct_loc = struct_id.lookup(db.upcast()); | ||
369 | let struct_src = struct_loc.source(db.upcast()); | ||
370 | |||
371 | if let Some(replacement) = struct_name_replacement { | ||
372 | let ast_ptr = match struct_src.value.name() { | ||
373 | Some(name) => name, | ||
374 | None => { | ||
375 | // We don't want rust-analyzer to panic over this, but it is definitely some kind of error in the logic. | ||
376 | log::error!( | ||
377 | "Replacement ({:?}) was generated for a structure without a name: {:?}", | ||
378 | replacement, | ||
379 | struct_src | ||
380 | ); | ||
381 | return; | ||
382 | } | ||
383 | }; | ||
384 | |||
385 | let diagnostic = IncorrectCase { | ||
386 | file: struct_src.file_id, | ||
387 | ident_type: "Structure".to_string(), | ||
388 | ident: AstPtr::new(&ast_ptr).into(), | ||
389 | expected_case: replacement.expected_case, | ||
390 | ident_text: replacement.current_name.to_string(), | ||
391 | suggested_text: replacement.suggested_text, | ||
392 | }; | ||
393 | |||
394 | self.sink.push(diagnostic); | ||
395 | } | ||
396 | |||
397 | let struct_fields_list = match struct_src.value.field_list() { | ||
398 | Some(ast::FieldList::RecordFieldList(fields)) => fields, | ||
399 | _ => { | ||
400 | if !struct_fields_replacements.is_empty() { | ||
401 | log::error!( | ||
402 | "Replacements ({:?}) were generated for a structure fields which had no fields list: {:?}", | ||
403 | struct_fields_replacements, struct_src | ||
404 | ); | ||
405 | } | ||
406 | return; | ||
407 | } | ||
408 | }; | ||
409 | let mut struct_fields_iter = struct_fields_list.fields(); | ||
410 | for field_to_rename in struct_fields_replacements { | ||
411 | // We assume that parameters in replacement are in the same order as in the | ||
412 | // actual params list, but just some of them (ones that named correctly) are skipped. | ||
413 | let ast_ptr = loop { | ||
414 | match struct_fields_iter.next() { | ||
415 | Some(element) if names_equal(element.name(), &field_to_rename.current_name) => { | ||
416 | break element.name().unwrap() | ||
417 | } | ||
418 | Some(_) => {} | ||
419 | None => { | ||
420 | log::error!( | ||
421 | "Replacement ({:?}) was generated for a structure field which was not found: {:?}", | ||
422 | field_to_rename, struct_src | ||
423 | ); | ||
424 | return; | ||
425 | } | ||
426 | } | ||
427 | }; | ||
428 | |||
429 | let diagnostic = IncorrectCase { | ||
430 | file: struct_src.file_id, | ||
431 | ident_type: "Field".to_string(), | ||
432 | ident: AstPtr::new(&ast_ptr).into(), | ||
433 | expected_case: field_to_rename.expected_case, | ||
434 | ident_text: field_to_rename.current_name.to_string(), | ||
435 | suggested_text: field_to_rename.suggested_text, | ||
436 | }; | ||
437 | |||
438 | self.sink.push(diagnostic); | ||
439 | } | ||
440 | } | ||
441 | |||
442 | fn validate_enum(&mut self, db: &dyn HirDatabase, enum_id: EnumId) { | ||
443 | let data = db.enum_data(enum_id); | ||
444 | |||
445 | // 1. Check the enum name. | ||
446 | let enum_name = data.name.to_string(); | ||
447 | let enum_name_replacement = if let Some(new_name) = to_camel_case(&enum_name) { | ||
448 | let replacement = Replacement { | ||
449 | current_name: data.name.clone(), | ||
450 | suggested_text: new_name, | ||
451 | expected_case: CaseType::UpperCamelCase, | ||
452 | }; | ||
453 | Some(replacement) | ||
454 | } else { | ||
455 | None | ||
456 | }; | ||
457 | |||
458 | // 2. Check the field names. | ||
459 | let mut enum_fields_replacements = Vec::new(); | ||
460 | |||
461 | for (_, variant) in data.variants.iter() { | ||
462 | let variant_name = variant.name.to_string(); | ||
463 | if let Some(new_name) = to_camel_case(&variant_name) { | ||
464 | let replacement = Replacement { | ||
465 | current_name: variant.name.clone(), | ||
466 | suggested_text: new_name, | ||
467 | expected_case: CaseType::UpperCamelCase, | ||
468 | }; | ||
469 | enum_fields_replacements.push(replacement); | ||
470 | } | ||
471 | } | ||
472 | |||
473 | // 3. If there is at least one element to spawn a warning on, go to the source map and generate a warning. | ||
474 | self.create_incorrect_case_diagnostic_for_enum( | ||
475 | enum_id, | ||
476 | db, | ||
477 | enum_name_replacement, | ||
478 | enum_fields_replacements, | ||
479 | ) | ||
480 | } | ||
481 | |||
482 | /// Given the information about incorrect names in the struct declaration, looks up into the source code | ||
483 | /// for exact locations and adds diagnostics into the sink. | ||
484 | fn create_incorrect_case_diagnostic_for_enum( | ||
485 | &mut self, | ||
486 | enum_id: EnumId, | ||
487 | db: &dyn HirDatabase, | ||
488 | enum_name_replacement: Option<Replacement>, | ||
489 | enum_variants_replacements: Vec<Replacement>, | ||
490 | ) { | ||
491 | // XXX: only look at sources if we do have incorrect names | ||
492 | if enum_name_replacement.is_none() && enum_variants_replacements.is_empty() { | ||
493 | return; | ||
494 | } | ||
495 | |||
496 | let enum_loc = enum_id.lookup(db.upcast()); | ||
497 | let enum_src = enum_loc.source(db.upcast()); | ||
498 | |||
499 | if let Some(replacement) = enum_name_replacement { | ||
500 | let ast_ptr = match enum_src.value.name() { | ||
501 | Some(name) => name, | ||
502 | None => { | ||
503 | // We don't want rust-analyzer to panic over this, but it is definitely some kind of error in the logic. | ||
504 | log::error!( | ||
505 | "Replacement ({:?}) was generated for a enum without a name: {:?}", | ||
506 | replacement, | ||
507 | enum_src | ||
508 | ); | ||
509 | return; | ||
510 | } | ||
511 | }; | ||
512 | |||
513 | let diagnostic = IncorrectCase { | ||
514 | file: enum_src.file_id, | ||
515 | ident_type: "Enum".to_string(), | ||
516 | ident: AstPtr::new(&ast_ptr).into(), | ||
517 | expected_case: replacement.expected_case, | ||
518 | ident_text: replacement.current_name.to_string(), | ||
519 | suggested_text: replacement.suggested_text, | ||
520 | }; | ||
521 | |||
522 | self.sink.push(diagnostic); | ||
523 | } | ||
524 | |||
525 | let enum_variants_list = match enum_src.value.variant_list() { | ||
526 | Some(variants) => variants, | ||
527 | _ => { | ||
528 | if !enum_variants_replacements.is_empty() { | ||
529 | log::error!( | ||
530 | "Replacements ({:?}) were generated for a enum variants which had no fields list: {:?}", | ||
531 | enum_variants_replacements, enum_src | ||
532 | ); | ||
533 | } | ||
534 | return; | ||
535 | } | ||
536 | }; | ||
537 | let mut enum_variants_iter = enum_variants_list.variants(); | ||
538 | for variant_to_rename in enum_variants_replacements { | ||
539 | // We assume that parameters in replacement are in the same order as in the | ||
540 | // actual params list, but just some of them (ones that named correctly) are skipped. | ||
541 | let ast_ptr = loop { | ||
542 | match enum_variants_iter.next() { | ||
543 | Some(variant) | ||
544 | if names_equal(variant.name(), &variant_to_rename.current_name) => | ||
545 | { | ||
546 | break variant.name().unwrap() | ||
547 | } | ||
548 | Some(_) => {} | ||
549 | None => { | ||
550 | log::error!( | ||
551 | "Replacement ({:?}) was generated for a enum variant which was not found: {:?}", | ||
552 | variant_to_rename, enum_src | ||
553 | ); | ||
554 | return; | ||
555 | } | ||
556 | } | ||
557 | }; | ||
558 | |||
559 | let diagnostic = IncorrectCase { | ||
560 | file: enum_src.file_id, | ||
561 | ident_type: "Variant".to_string(), | ||
562 | ident: AstPtr::new(&ast_ptr).into(), | ||
563 | expected_case: variant_to_rename.expected_case, | ||
564 | ident_text: variant_to_rename.current_name.to_string(), | ||
565 | suggested_text: variant_to_rename.suggested_text, | ||
566 | }; | ||
567 | |||
568 | self.sink.push(diagnostic); | ||
569 | } | ||
570 | } | ||
571 | |||
572 | fn validate_const(&mut self, db: &dyn HirDatabase, const_id: ConstId) { | ||
573 | let data = db.const_data(const_id); | ||
574 | |||
575 | let name = match &data.name { | ||
576 | Some(name) => name, | ||
577 | None => return, | ||
578 | }; | ||
579 | |||
580 | let const_name = name.to_string(); | ||
581 | let replacement = if let Some(new_name) = to_upper_snake_case(&const_name) { | ||
582 | Replacement { | ||
583 | current_name: name.clone(), | ||
584 | suggested_text: new_name, | ||
585 | expected_case: CaseType::UpperSnakeCase, | ||
586 | } | ||
587 | } else { | ||
588 | // Nothing to do here. | ||
589 | return; | ||
590 | }; | ||
591 | |||
592 | let const_loc = const_id.lookup(db.upcast()); | ||
593 | let const_src = const_loc.source(db.upcast()); | ||
594 | |||
595 | let ast_ptr = match const_src.value.name() { | ||
596 | Some(name) => name, | ||
597 | None => return, | ||
598 | }; | ||
599 | |||
600 | let diagnostic = IncorrectCase { | ||
601 | file: const_src.file_id, | ||
602 | ident_type: "Constant".to_string(), | ||
603 | ident: AstPtr::new(&ast_ptr).into(), | ||
604 | expected_case: replacement.expected_case, | ||
605 | ident_text: replacement.current_name.to_string(), | ||
606 | suggested_text: replacement.suggested_text, | ||
607 | }; | ||
608 | |||
609 | self.sink.push(diagnostic); | ||
610 | } | ||
611 | |||
612 | fn validate_static(&mut self, db: &dyn HirDatabase, static_id: StaticId) { | ||
613 | let data = db.static_data(static_id); | ||
614 | |||
615 | let name = match &data.name { | ||
616 | Some(name) => name, | ||
617 | None => return, | ||
618 | }; | ||
619 | |||
620 | let static_name = name.to_string(); | ||
621 | let replacement = if let Some(new_name) = to_upper_snake_case(&static_name) { | ||
622 | Replacement { | ||
623 | current_name: name.clone(), | ||
624 | suggested_text: new_name, | ||
625 | expected_case: CaseType::UpperSnakeCase, | ||
626 | } | ||
627 | } else { | ||
628 | // Nothing to do here. | ||
629 | return; | ||
630 | }; | ||
631 | |||
632 | let static_loc = static_id.lookup(db.upcast()); | ||
633 | let static_src = static_loc.source(db.upcast()); | ||
634 | |||
635 | let ast_ptr = match static_src.value.name() { | ||
636 | Some(name) => name, | ||
637 | None => return, | ||
638 | }; | ||
639 | |||
640 | let diagnostic = IncorrectCase { | ||
641 | file: static_src.file_id, | ||
642 | ident_type: "Static variable".to_string(), | ||
643 | ident: AstPtr::new(&ast_ptr).into(), | ||
644 | expected_case: replacement.expected_case, | ||
645 | ident_text: replacement.current_name.to_string(), | ||
646 | suggested_text: replacement.suggested_text, | ||
647 | }; | ||
648 | |||
649 | self.sink.push(diagnostic); | ||
650 | } | ||
651 | } | ||
652 | |||
653 | fn names_equal(left: Option<ast::Name>, right: &Name) -> bool { | ||
654 | if let Some(left) = left { | ||
655 | &left.as_name() == right | ||
656 | } else { | ||
657 | false | ||
658 | } | ||
659 | } | ||
660 | |||
661 | fn pat_equals_to_name(pat: Option<ast::Pat>, name: &Name) -> bool { | ||
662 | if let Some(ast::Pat::IdentPat(ident)) = pat { | ||
663 | ident.to_string() == name.to_string() | ||
664 | } else { | ||
665 | false | ||
666 | } | ||
667 | } | ||
668 | |||
669 | #[cfg(test)] | ||
670 | mod tests { | ||
671 | use crate::diagnostics::tests::check_diagnostics; | ||
672 | |||
673 | #[test] | ||
674 | fn incorrect_function_name() { | ||
675 | check_diagnostics( | ||
676 | r#" | ||
677 | fn NonSnakeCaseName() {} | ||
678 | // ^^^^^^^^^^^^^^^^ Function `NonSnakeCaseName` should have snake_case name, e.g. `non_snake_case_name` | ||
679 | "#, | ||
680 | ); | ||
681 | } | ||
682 | |||
683 | #[test] | ||
684 | fn incorrect_function_params() { | ||
685 | check_diagnostics( | ||
686 | r#" | ||
687 | fn foo(SomeParam: u8) {} | ||
688 | // ^^^^^^^^^ Argument `SomeParam` should have snake_case name, e.g. `some_param` | ||
689 | |||
690 | fn foo2(ok_param: &str, CAPS_PARAM: u8) {} | ||
691 | // ^^^^^^^^^^ Argument `CAPS_PARAM` should have snake_case name, e.g. `caps_param` | ||
692 | "#, | ||
693 | ); | ||
694 | } | ||
695 | |||
696 | #[test] | ||
697 | fn incorrect_variable_names() { | ||
698 | check_diagnostics( | ||
699 | r#" | ||
700 | fn foo() { | ||
701 | let SOME_VALUE = 10; | ||
702 | // ^^^^^^^^^^ Variable `SOME_VALUE` should have snake_case name, e.g. `some_value` | ||
703 | let AnotherValue = 20; | ||
704 | // ^^^^^^^^^^^^ Variable `AnotherValue` should have snake_case name, e.g. `another_value` | ||
705 | } | ||
706 | "#, | ||
707 | ); | ||
708 | } | ||
709 | |||
710 | #[test] | ||
711 | fn incorrect_struct_name() { | ||
712 | check_diagnostics( | ||
713 | r#" | ||
714 | struct non_camel_case_name {} | ||
715 | // ^^^^^^^^^^^^^^^^^^^ Structure `non_camel_case_name` should have CamelCase name, e.g. `NonCamelCaseName` | ||
716 | "#, | ||
717 | ); | ||
718 | } | ||
719 | |||
720 | #[test] | ||
721 | fn incorrect_struct_field() { | ||
722 | check_diagnostics( | ||
723 | r#" | ||
724 | struct SomeStruct { SomeField: u8 } | ||
725 | // ^^^^^^^^^ Field `SomeField` should have snake_case name, e.g. `some_field` | ||
726 | "#, | ||
727 | ); | ||
728 | } | ||
729 | |||
730 | #[test] | ||
731 | fn incorrect_enum_name() { | ||
732 | check_diagnostics( | ||
733 | r#" | ||
734 | enum some_enum { Val(u8) } | ||
735 | // ^^^^^^^^^ Enum `some_enum` should have CamelCase name, e.g. `SomeEnum` | ||
736 | "#, | ||
737 | ); | ||
738 | } | ||
739 | |||
740 | #[test] | ||
741 | fn incorrect_enum_variant_name() { | ||
742 | check_diagnostics( | ||
743 | r#" | ||
744 | enum SomeEnum { SOME_VARIANT(u8) } | ||
745 | // ^^^^^^^^^^^^ Variant `SOME_VARIANT` should have CamelCase name, e.g. `SomeVariant` | ||
746 | "#, | ||
747 | ); | ||
748 | } | ||
749 | |||
750 | #[test] | ||
751 | fn incorrect_const_name() { | ||
752 | check_diagnostics( | ||
753 | r#" | ||
754 | const some_weird_const: u8 = 10; | ||
755 | // ^^^^^^^^^^^^^^^^ Constant `some_weird_const` should have UPPER_SNAKE_CASE name, e.g. `SOME_WEIRD_CONST` | ||
756 | |||
757 | fn func() { | ||
758 | const someConstInFunc: &str = "hi there"; | ||
759 | // ^^^^^^^^^^^^^^^ Constant `someConstInFunc` should have UPPER_SNAKE_CASE name, e.g. `SOME_CONST_IN_FUNC` | ||
760 | |||
761 | } | ||
762 | "#, | ||
763 | ); | ||
764 | } | ||
765 | |||
766 | #[test] | ||
767 | fn incorrect_static_name() { | ||
768 | check_diagnostics( | ||
769 | r#" | ||
770 | static some_weird_const: u8 = 10; | ||
771 | // ^^^^^^^^^^^^^^^^ Static variable `some_weird_const` should have UPPER_SNAKE_CASE name, e.g. `SOME_WEIRD_CONST` | ||
772 | |||
773 | fn func() { | ||
774 | static someConstInFunc: &str = "hi there"; | ||
775 | // ^^^^^^^^^^^^^^^ Static variable `someConstInFunc` should have UPPER_SNAKE_CASE name, e.g. `SOME_CONST_IN_FUNC` | ||
776 | } | ||
777 | "#, | ||
778 | ); | ||
779 | } | ||
780 | |||
781 | #[test] | ||
782 | fn fn_inside_impl_struct() { | ||
783 | check_diagnostics( | ||
784 | r#" | ||
785 | struct someStruct; | ||
786 | // ^^^^^^^^^^ Structure `someStruct` should have CamelCase name, e.g. `SomeStruct` | ||
787 | |||
788 | impl someStruct { | ||
789 | fn SomeFunc(&self) { | ||
790 | // ^^^^^^^^ Function `SomeFunc` should have snake_case name, e.g. `some_func` | ||
791 | static someConstInFunc: &str = "hi there"; | ||
792 | // ^^^^^^^^^^^^^^^ Static variable `someConstInFunc` should have UPPER_SNAKE_CASE name, e.g. `SOME_CONST_IN_FUNC` | ||
793 | let WHY_VAR_IS_CAPS = 10; | ||
794 | // ^^^^^^^^^^^^^^^ Variable `WHY_VAR_IS_CAPS` should have snake_case name, e.g. `why_var_is_caps` | ||
795 | } | ||
796 | } | ||
797 | "#, | ||
798 | ); | ||
799 | } | ||
800 | |||
801 | #[test] | ||
802 | fn no_diagnostic_for_enum_varinats() { | ||
803 | check_diagnostics( | ||
804 | r#" | ||
805 | enum Option { Some, None } | ||
806 | |||
807 | fn main() { | ||
808 | match Option::None { | ||
809 | None => (), | ||
810 | Some => (), | ||
811 | } | ||
812 | } | ||
813 | "#, | ||
814 | ); | ||
815 | } | ||
816 | |||
817 | #[test] | ||
818 | fn non_let_bind() { | ||
819 | check_diagnostics( | ||
820 | r#" | ||
821 | enum Option { Some, None } | ||
822 | |||
823 | fn main() { | ||
824 | match Option::None { | ||
825 | SOME_VAR @ None => (), | ||
826 | // ^^^^^^^^ Variable `SOME_VAR` should have snake_case name, e.g. `some_var` | ||
827 | Some => (), | ||
828 | } | ||
829 | } | ||
830 | "#, | ||
831 | ); | ||
832 | } | ||
833 | } | ||
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 | } | ||
diff --git a/crates/hir_ty/src/diagnostics/unsafe_check.rs b/crates/hir_ty/src/diagnostics/unsafe_check.rs index 61ffbf5d1..21a121aad 100644 --- a/crates/hir_ty/src/diagnostics/unsafe_check.rs +++ b/crates/hir_ty/src/diagnostics/unsafe_check.rs | |||
@@ -190,13 +190,13 @@ struct Ty { | |||
190 | a: u8, | 190 | a: u8, |
191 | } | 191 | } |
192 | 192 | ||
193 | static mut static_mut: Ty = Ty { a: 0 }; | 193 | static mut STATIC_MUT: Ty = Ty { a: 0 }; |
194 | 194 | ||
195 | fn main() { | 195 | fn main() { |
196 | let x = static_mut.a; | 196 | let x = STATIC_MUT.a; |
197 | //^^^^^^^^^^ This operation is unsafe and requires an unsafe function or block | 197 | //^^^^^^^^^^ This operation is unsafe and requires an unsafe function or block |
198 | unsafe { | 198 | unsafe { |
199 | let x = static_mut.a; | 199 | let x = STATIC_MUT.a; |
200 | } | 200 | } |
201 | } | 201 | } |
202 | "#, | 202 | "#, |