aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/hir/src/diagnostics.rs29
-rw-r--r--crates/hir/src/lib.rs18
-rw-r--r--crates/hir_ty/src/diagnostics.rs3
-rw-r--r--crates/ide/src/diagnostics.rs530
-rw-r--r--crates/ide/src/diagnostics/fixes.rs25
-rw-r--r--crates/ide/src/diagnostics/fixes/change_case.rs155
-rw-r--r--crates/ide/src/diagnostics/incorrect_case.rs488
-rw-r--r--crates/ide/src/diagnostics/unlinked_file.rs259
8 files changed, 713 insertions, 794 deletions
diff --git a/crates/hir/src/diagnostics.rs b/crates/hir/src/diagnostics.rs
index c294a803b..c2d608eb5 100644
--- a/crates/hir/src/diagnostics.rs
+++ b/crates/hir/src/diagnostics.rs
@@ -34,6 +34,7 @@ macro_rules! diagnostics {
34diagnostics![ 34diagnostics![
35 BreakOutsideOfLoop, 35 BreakOutsideOfLoop,
36 InactiveCode, 36 InactiveCode,
37 IncorrectCase,
37 MacroError, 38 MacroError,
38 MismatchedArgCount, 39 MismatchedArgCount,
39 MissingFields, 40 MissingFields,
@@ -195,31 +196,3 @@ impl Diagnostic for InternalBailedOut {
195} 196}
196 197
197pub use hir_ty::diagnostics::IncorrectCase; 198pub use hir_ty::diagnostics::IncorrectCase;
198
199impl Diagnostic for IncorrectCase {
200 fn code(&self) -> DiagnosticCode {
201 DiagnosticCode("incorrect-ident-case")
202 }
203
204 fn message(&self) -> String {
205 format!(
206 "{} `{}` should have {} name, e.g. `{}`",
207 self.ident_type,
208 self.ident_text,
209 self.expected_case.to_string(),
210 self.suggested_text
211 )
212 }
213
214 fn display_source(&self) -> InFile<SyntaxNodePtr> {
215 InFile::new(self.file, self.ident.clone().into())
216 }
217
218 fn as_any(&self) -> &(dyn Any + Send + 'static) {
219 self
220 }
221
222 fn is_experimental(&self) -> bool {
223 true
224 }
225}
diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs
index b2731b62f..fc147ade3 100644
--- a/crates/hir/src/lib.rs
+++ b/crates/hir/src/lib.rs
@@ -86,8 +86,8 @@ use crate::{
86pub use crate::{ 86pub use crate::{
87 attrs::{HasAttrs, Namespace}, 87 attrs::{HasAttrs, Namespace},
88 diagnostics::{ 88 diagnostics::{
89 AnyDiagnostic, BreakOutsideOfLoop, InactiveCode, InternalBailedOut, MacroError, 89 AnyDiagnostic, BreakOutsideOfLoop, InactiveCode, IncorrectCase, InternalBailedOut,
90 MismatchedArgCount, MissingFields, MissingMatchArms, MissingOkOrSomeInTailExpr, 90 MacroError, MismatchedArgCount, MissingFields, MissingMatchArms, MissingOkOrSomeInTailExpr,
91 MissingUnsafe, NoSuchField, RemoveThisSemicolon, ReplaceFilterMapNextWithFindMap, 91 MissingUnsafe, NoSuchField, RemoveThisSemicolon, ReplaceFilterMapNextWithFindMap,
92 UnimplementedBuiltinMacro, UnresolvedExternCrate, UnresolvedImport, UnresolvedMacroCall, 92 UnimplementedBuiltinMacro, UnresolvedExternCrate, UnresolvedImport, UnresolvedMacroCall,
93 UnresolvedModule, UnresolvedProcMacro, 93 UnresolvedModule, UnresolvedProcMacro,
@@ -340,7 +340,7 @@ impl ModuleDef {
340 } 340 }
341 } 341 }
342 342
343 pub fn diagnostics(self, db: &dyn HirDatabase, sink: &mut DiagnosticSink) { 343 pub fn diagnostics(self, db: &dyn HirDatabase) -> Vec<AnyDiagnostic> {
344 let id = match self { 344 let id = match self {
345 ModuleDef::Adt(it) => match it { 345 ModuleDef::Adt(it) => match it {
346 Adt::Struct(it) => it.id.into(), 346 Adt::Struct(it) => it.id.into(),
@@ -353,17 +353,19 @@ impl ModuleDef {
353 ModuleDef::Module(it) => it.id.into(), 353 ModuleDef::Module(it) => it.id.into(),
354 ModuleDef::Const(it) => it.id.into(), 354 ModuleDef::Const(it) => it.id.into(),
355 ModuleDef::Static(it) => it.id.into(), 355 ModuleDef::Static(it) => it.id.into(),
356 _ => return, 356 _ => return Vec::new(),
357 }; 357 };
358 358
359 let module = match self.module(db) { 359 let module = match self.module(db) {
360 Some(it) => it, 360 Some(it) => it,
361 None => return, 361 None => return Vec::new(),
362 }; 362 };
363 363
364 let mut acc = Vec::new();
364 for diag in hir_ty::diagnostics::validate_module_item(db, module.id.krate(), id) { 365 for diag in hir_ty::diagnostics::validate_module_item(db, module.id.krate(), id) {
365 sink.push(diag) 366 acc.push(diag.into())
366 } 367 }
368 acc
367 } 369 }
368} 370}
369 371
@@ -624,7 +626,7 @@ impl Module {
624 acc.extend(m.diagnostics(db, sink, internal_diagnostics)) 626 acc.extend(m.diagnostics(db, sink, internal_diagnostics))
625 } 627 }
626 } 628 }
627 _ => decl.diagnostics(db, sink), 629 _ => acc.extend(decl.diagnostics(db)),
628 } 630 }
629 } 631 }
630 632
@@ -1234,7 +1236,7 @@ impl Function {
1234 } 1236 }
1235 1237
1236 for diag in hir_ty::diagnostics::validate_module_item(db, krate, self.id.into()) { 1238 for diag in hir_ty::diagnostics::validate_module_item(db, krate, self.id.into()) {
1237 sink.push(diag) 1239 acc.push(diag.into())
1238 } 1240 }
1239 acc 1241 acc
1240 } 1242 }
diff --git a/crates/hir_ty/src/diagnostics.rs b/crates/hir_ty/src/diagnostics.rs
index 407273943..6339c9687 100644
--- a/crates/hir_ty/src/diagnostics.rs
+++ b/crates/hir_ty/src/diagnostics.rs
@@ -84,9 +84,6 @@ impl fmt::Display for IdentType {
84 } 84 }
85} 85}
86 86
87// Diagnostic: incorrect-ident-case
88//
89// This diagnostic is triggered if an item name doesn't follow https://doc.rust-lang.org/1.0.0/style/style/naming/README.html[Rust naming convention].
90#[derive(Debug)] 87#[derive(Debug)]
91pub struct IncorrectCase { 88pub struct IncorrectCase {
92 pub file: HirFileId, 89 pub file: HirFileId,
diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs
index 814e64ae4..7978c1fc2 100644
--- a/crates/ide/src/diagnostics.rs
+++ b/crates/ide/src/diagnostics.rs
@@ -6,6 +6,7 @@
6 6
7mod break_outside_of_loop; 7mod break_outside_of_loop;
8mod inactive_code; 8mod inactive_code;
9mod incorrect_case;
9mod macro_error; 10mod macro_error;
10mod mismatched_arg_count; 11mod mismatched_arg_count;
11mod missing_fields; 12mod missing_fields;
@@ -15,20 +16,19 @@ mod no_such_field;
15mod remove_this_semicolon; 16mod remove_this_semicolon;
16mod replace_filter_map_next_with_find_map; 17mod replace_filter_map_next_with_find_map;
17mod unimplemented_builtin_macro; 18mod unimplemented_builtin_macro;
19mod unlinked_file;
18mod unresolved_extern_crate; 20mod unresolved_extern_crate;
19mod unresolved_import; 21mod unresolved_import;
20mod unresolved_macro_call; 22mod unresolved_macro_call;
21mod unresolved_module; 23mod unresolved_module;
22mod unresolved_proc_macro; 24mod unresolved_proc_macro;
23 25
24mod fixes;
25mod field_shorthand; 26mod field_shorthand;
26mod unlinked_file;
27 27
28use std::cell::RefCell; 28use std::cell::RefCell;
29 29
30use hir::{ 30use hir::{
31 diagnostics::{AnyDiagnostic, Diagnostic as _, DiagnosticCode, DiagnosticSinkBuilder}, 31 diagnostics::{AnyDiagnostic, DiagnosticCode, DiagnosticSinkBuilder},
32 Semantics, 32 Semantics,
33}; 33};
34use ide_assists::AssistResolveStrategy; 34use ide_assists::AssistResolveStrategy;
@@ -37,15 +37,13 @@ use itertools::Itertools;
37use rustc_hash::FxHashSet; 37use rustc_hash::FxHashSet;
38use syntax::{ 38use syntax::{
39 ast::{self, AstNode}, 39 ast::{self, AstNode},
40 SyntaxNode, SyntaxNodePtr, TextRange, TextSize, 40 SyntaxNode, TextRange,
41}; 41};
42use text_edit::TextEdit; 42use text_edit::TextEdit;
43use unlinked_file::UnlinkedFile; 43use unlinked_file::UnlinkedFile;
44 44
45use crate::{Assist, AssistId, AssistKind, FileId, Label, SourceChange}; 45use crate::{Assist, AssistId, AssistKind, FileId, Label, SourceChange};
46 46
47use self::fixes::DiagnosticWithFixes;
48
49#[derive(Debug)] 47#[derive(Debug)]
50pub struct Diagnostic { 48pub struct Diagnostic {
51 // pub name: Option<String>, 49 // pub name: Option<String>,
@@ -135,7 +133,6 @@ pub struct DiagnosticsConfig {
135struct DiagnosticsContext<'a> { 133struct DiagnosticsContext<'a> {
136 config: &'a DiagnosticsConfig, 134 config: &'a DiagnosticsConfig,
137 sema: Semantics<'a, RootDatabase>, 135 sema: Semantics<'a, RootDatabase>,
138 #[allow(unused)]
139 resolve: &'a AssistResolveStrategy, 136 resolve: &'a AssistResolveStrategy,
140} 137}
141 138
@@ -165,22 +162,6 @@ pub(crate) fn diagnostics(
165 } 162 }
166 let res = RefCell::new(res); 163 let res = RefCell::new(res);
167 let sink_builder = DiagnosticSinkBuilder::new() 164 let sink_builder = DiagnosticSinkBuilder::new()
168 .on::<hir::diagnostics::IncorrectCase, _>(|d| {
169 res.borrow_mut().push(warning_with_fix(d, &sema, resolve));
170 })
171 .on::<UnlinkedFile, _>(|d| {
172 // Limit diagnostic to the first few characters in the file. This matches how VS Code
173 // renders it with the full span, but on other editors, and is less invasive.
174 let range = sema.diagnostics_display_range(d.display_source()).range;
175 let range = range.intersect(TextRange::up_to(TextSize::of("..."))).unwrap_or(range);
176
177 // Override severity and mark as unused.
178 res.borrow_mut().push(
179 Diagnostic::hint(range, d.message())
180 .with_fixes(d.fixes(&sema, resolve))
181 .with_code(Some(d.code())),
182 );
183 })
184 // Only collect experimental diagnostics when they're enabled. 165 // Only collect experimental diagnostics when they're enabled.
185 .filter(|diag| !(diag.is_experimental() && config.disable_experimental)) 166 .filter(|diag| !(diag.is_experimental() && config.disable_experimental))
186 .filter(|diag| !config.disabled.contains(diag.code().as_str())); 167 .filter(|diag| !config.disabled.contains(diag.code().as_str()));
@@ -200,11 +181,9 @@ pub(crate) fn diagnostics(
200 181
201 let mut diags = Vec::new(); 182 let mut diags = Vec::new();
202 let internal_diagnostics = cfg!(test); 183 let internal_diagnostics = cfg!(test);
203 match sema.to_module_def(file_id) { 184 let module = sema.to_module_def(file_id);
204 Some(m) => diags = m.diagnostics(db, &mut sink, internal_diagnostics), 185 if let Some(m) = module {
205 None => { 186 diags = m.diagnostics(db, &mut sink, internal_diagnostics)
206 sink.push(UnlinkedFile { file_id, node: SyntaxNodePtr::new(parse.tree().syntax()) });
207 }
208 } 187 }
209 188
210 drop(sink); 189 drop(sink);
@@ -212,10 +191,17 @@ pub(crate) fn diagnostics(
212 let mut res = res.into_inner(); 191 let mut res = res.into_inner();
213 192
214 let ctx = DiagnosticsContext { config, sema, resolve }; 193 let ctx = DiagnosticsContext { config, sema, resolve };
194 if module.is_none() {
195 let d = UnlinkedFile { file: file_id };
196 let d = unlinked_file::unlinked_file(&ctx, &d);
197 res.push(d)
198 }
199
215 for diag in diags { 200 for diag in diags {
216 #[rustfmt::skip] 201 #[rustfmt::skip]
217 let d = match diag { 202 let d = match diag {
218 AnyDiagnostic::BreakOutsideOfLoop(d) => break_outside_of_loop::break_outside_of_loop(&ctx, &d), 203 AnyDiagnostic::BreakOutsideOfLoop(d) => break_outside_of_loop::break_outside_of_loop(&ctx, &d),
204 AnyDiagnostic::IncorrectCase(d) => incorrect_case::incorrect_case(&ctx, &d),
219 AnyDiagnostic::MacroError(d) => macro_error::macro_error(&ctx, &d), 205 AnyDiagnostic::MacroError(d) => macro_error::macro_error(&ctx, &d),
220 AnyDiagnostic::MismatchedArgCount(d) => mismatched_arg_count::mismatched_arg_count(&ctx, &d), 206 AnyDiagnostic::MismatchedArgCount(d) => mismatched_arg_count::mismatched_arg_count(&ctx, &d),
221 AnyDiagnostic::MissingFields(d) => missing_fields::missing_fields(&ctx, &d), 207 AnyDiagnostic::MissingFields(d) => missing_fields::missing_fields(&ctx, &d),
@@ -236,30 +222,24 @@ pub(crate) fn diagnostics(
236 None => continue, 222 None => continue,
237 } 223 }
238 }; 224 };
225 res.push(d)
226 }
227
228 res.retain(|d| {
239 if let Some(code) = d.code { 229 if let Some(code) = d.code {
240 if ctx.config.disabled.contains(code.as_str()) { 230 if ctx.config.disabled.contains(code.as_str()) {
241 continue; 231 return false;
242 } 232 }
243 } 233 }
244 if ctx.config.disable_experimental && d.experimental { 234 if ctx.config.disable_experimental && d.experimental {
245 continue; 235 return false;
246 } 236 }
247 res.push(d) 237 true
248 } 238 });
249 239
250 res 240 res
251} 241}
252 242
253fn warning_with_fix<D: DiagnosticWithFixes>(
254 d: &D,
255 sema: &Semantics<RootDatabase>,
256 resolve: &AssistResolveStrategy,
257) -> Diagnostic {
258 Diagnostic::hint(sema.diagnostics_display_range(d.display_source()).range, d.message())
259 .with_fixes(d.fixes(sema, resolve))
260 .with_code(Some(d.code()))
261}
262
263fn check_unnecessary_braces_in_use_statement( 243fn check_unnecessary_braces_in_use_statement(
264 acc: &mut Vec<Diagnostic>, 244 acc: &mut Vec<Diagnostic>,
265 file_id: FileId, 245 file_id: FileId,
@@ -390,8 +370,9 @@ mod tests {
390 file_position.offset 370 file_position.offset
391 ); 371 );
392 } 372 }
373
393 /// Checks that there's a diagnostic *without* fix at `$0`. 374 /// Checks that there's a diagnostic *without* fix at `$0`.
394 fn check_no_fix(ra_fixture: &str) { 375 pub(crate) fn check_no_fix(ra_fixture: &str) {
395 let (analysis, file_position) = fixture::position(ra_fixture); 376 let (analysis, file_position) = fixture::position(ra_fixture);
396 let diagnostic = analysis 377 let diagnostic = analysis
397 .diagnostics( 378 .diagnostics(
@@ -536,142 +517,6 @@ mod a {
536 } 517 }
537 518
538 #[test] 519 #[test]
539 fn unlinked_file_prepend_first_item() {
540 cov_mark::check!(unlinked_file_prepend_before_first_item);
541 // Only tests the first one for `pub mod` since the rest are the same
542 check_fixes(
543 r#"
544//- /main.rs
545fn f() {}
546//- /foo.rs
547$0
548"#,
549 vec![
550 r#"
551mod foo;
552
553fn f() {}
554"#,
555 r#"
556pub mod foo;
557
558fn f() {}
559"#,
560 ],
561 );
562 }
563
564 #[test]
565 fn unlinked_file_append_mod() {
566 cov_mark::check!(unlinked_file_append_to_existing_mods);
567 check_fix(
568 r#"
569//- /main.rs
570//! Comment on top
571
572mod preexisting;
573
574mod preexisting2;
575
576struct S;
577
578mod preexisting_bottom;)
579//- /foo.rs
580$0
581"#,
582 r#"
583//! Comment on top
584
585mod preexisting;
586
587mod preexisting2;
588mod foo;
589
590struct S;
591
592mod preexisting_bottom;)
593"#,
594 );
595 }
596
597 #[test]
598 fn unlinked_file_insert_in_empty_file() {
599 cov_mark::check!(unlinked_file_empty_file);
600 check_fix(
601 r#"
602//- /main.rs
603//- /foo.rs
604$0
605"#,
606 r#"
607mod foo;
608"#,
609 );
610 }
611
612 #[test]
613 fn unlinked_file_old_style_modrs() {
614 check_fix(
615 r#"
616//- /main.rs
617mod submod;
618//- /submod/mod.rs
619// in mod.rs
620//- /submod/foo.rs
621$0
622"#,
623 r#"
624// in mod.rs
625mod foo;
626"#,
627 );
628 }
629
630 #[test]
631 fn unlinked_file_new_style_mod() {
632 check_fix(
633 r#"
634//- /main.rs
635mod submod;
636//- /submod.rs
637//- /submod/foo.rs
638$0
639"#,
640 r#"
641mod foo;
642"#,
643 );
644 }
645
646 #[test]
647 fn unlinked_file_with_cfg_off() {
648 cov_mark::check!(unlinked_file_skip_fix_when_mod_already_exists);
649 check_no_fix(
650 r#"
651//- /main.rs
652#[cfg(never)]
653mod foo;
654
655//- /foo.rs
656$0
657"#,
658 );
659 }
660
661 #[test]
662 fn unlinked_file_with_cfg_on() {
663 check_diagnostics(
664 r#"
665//- /main.rs
666#[cfg(not(never))]
667mod foo;
668
669//- /foo.rs
670"#,
671 );
672 }
673
674 #[test]
675 fn import_extern_crate_clash_with_inner_item() { 520 fn import_extern_crate_clash_with_inner_item() {
676 // This is more of a resolver test, but doesn't really work with the hir_def testsuite. 521 // This is more of a resolver test, but doesn't really work with the hir_def testsuite.
677 522
@@ -1607,330 +1452,3 @@ fn main() {
1607 } 1452 }
1608 } 1453 }
1609} 1454}
1610
1611#[cfg(test)]
1612mod decl_check_tests {
1613 use crate::diagnostics::tests::check_diagnostics;
1614
1615 #[test]
1616 fn incorrect_function_name() {
1617 check_diagnostics(
1618 r#"
1619fn NonSnakeCaseName() {}
1620// ^^^^^^^^^^^^^^^^ Function `NonSnakeCaseName` should have snake_case name, e.g. `non_snake_case_name`
1621"#,
1622 );
1623 }
1624
1625 #[test]
1626 fn incorrect_function_params() {
1627 check_diagnostics(
1628 r#"
1629fn foo(SomeParam: u8) {}
1630 // ^^^^^^^^^ Parameter `SomeParam` should have snake_case name, e.g. `some_param`
1631
1632fn foo2(ok_param: &str, CAPS_PARAM: u8) {}
1633 // ^^^^^^^^^^ Parameter `CAPS_PARAM` should have snake_case name, e.g. `caps_param`
1634"#,
1635 );
1636 }
1637
1638 #[test]
1639 fn incorrect_variable_names() {
1640 check_diagnostics(
1641 r#"
1642fn foo() {
1643 let SOME_VALUE = 10;
1644 // ^^^^^^^^^^ Variable `SOME_VALUE` should have snake_case name, e.g. `some_value`
1645 let AnotherValue = 20;
1646 // ^^^^^^^^^^^^ Variable `AnotherValue` should have snake_case name, e.g. `another_value`
1647}
1648"#,
1649 );
1650 }
1651
1652 #[test]
1653 fn incorrect_struct_names() {
1654 check_diagnostics(
1655 r#"
1656struct non_camel_case_name {}
1657 // ^^^^^^^^^^^^^^^^^^^ Structure `non_camel_case_name` should have CamelCase name, e.g. `NonCamelCaseName`
1658
1659struct SCREAMING_CASE {}
1660 // ^^^^^^^^^^^^^^ Structure `SCREAMING_CASE` should have CamelCase name, e.g. `ScreamingCase`
1661"#,
1662 );
1663 }
1664
1665 #[test]
1666 fn no_diagnostic_for_camel_cased_acronyms_in_struct_name() {
1667 check_diagnostics(
1668 r#"
1669struct AABB {}
1670"#,
1671 );
1672 }
1673
1674 #[test]
1675 fn incorrect_struct_field() {
1676 check_diagnostics(
1677 r#"
1678struct SomeStruct { SomeField: u8 }
1679 // ^^^^^^^^^ Field `SomeField` should have snake_case name, e.g. `some_field`
1680"#,
1681 );
1682 }
1683
1684 #[test]
1685 fn incorrect_enum_names() {
1686 check_diagnostics(
1687 r#"
1688enum some_enum { Val(u8) }
1689 // ^^^^^^^^^ Enum `some_enum` should have CamelCase name, e.g. `SomeEnum`
1690
1691enum SOME_ENUM {}
1692 // ^^^^^^^^^ Enum `SOME_ENUM` should have CamelCase name, e.g. `SomeEnum`
1693"#,
1694 );
1695 }
1696
1697 #[test]
1698 fn no_diagnostic_for_camel_cased_acronyms_in_enum_name() {
1699 check_diagnostics(
1700 r#"
1701enum AABB {}
1702"#,
1703 );
1704 }
1705
1706 #[test]
1707 fn incorrect_enum_variant_name() {
1708 check_diagnostics(
1709 r#"
1710enum SomeEnum { SOME_VARIANT(u8) }
1711 // ^^^^^^^^^^^^ Variant `SOME_VARIANT` should have CamelCase name, e.g. `SomeVariant`
1712"#,
1713 );
1714 }
1715
1716 #[test]
1717 fn incorrect_const_name() {
1718 check_diagnostics(
1719 r#"
1720const some_weird_const: u8 = 10;
1721 // ^^^^^^^^^^^^^^^^ Constant `some_weird_const` should have UPPER_SNAKE_CASE name, e.g. `SOME_WEIRD_CONST`
1722"#,
1723 );
1724 }
1725
1726 #[test]
1727 fn incorrect_static_name() {
1728 check_diagnostics(
1729 r#"
1730static some_weird_const: u8 = 10;
1731 // ^^^^^^^^^^^^^^^^ Static variable `some_weird_const` should have UPPER_SNAKE_CASE name, e.g. `SOME_WEIRD_CONST`
1732"#,
1733 );
1734 }
1735
1736 #[test]
1737 fn fn_inside_impl_struct() {
1738 check_diagnostics(
1739 r#"
1740struct someStruct;
1741 // ^^^^^^^^^^ Structure `someStruct` should have CamelCase name, e.g. `SomeStruct`
1742
1743impl someStruct {
1744 fn SomeFunc(&self) {
1745 // ^^^^^^^^ Function `SomeFunc` should have snake_case name, e.g. `some_func`
1746 let WHY_VAR_IS_CAPS = 10;
1747 // ^^^^^^^^^^^^^^^ Variable `WHY_VAR_IS_CAPS` should have snake_case name, e.g. `why_var_is_caps`
1748 }
1749}
1750"#,
1751 );
1752 }
1753
1754 #[test]
1755 fn no_diagnostic_for_enum_varinats() {
1756 check_diagnostics(
1757 r#"
1758enum Option { Some, None }
1759
1760fn main() {
1761 match Option::None {
1762 None => (),
1763 Some => (),
1764 }
1765}
1766"#,
1767 );
1768 }
1769
1770 #[test]
1771 fn non_let_bind() {
1772 check_diagnostics(
1773 r#"
1774enum Option { Some, None }
1775
1776fn main() {
1777 match Option::None {
1778 SOME_VAR @ None => (),
1779 // ^^^^^^^^ Variable `SOME_VAR` should have snake_case name, e.g. `some_var`
1780 Some => (),
1781 }
1782}
1783"#,
1784 );
1785 }
1786
1787 #[test]
1788 fn allow_attributes() {
1789 check_diagnostics(
1790 r#"
1791#[allow(non_snake_case)]
1792fn NonSnakeCaseName(SOME_VAR: u8) -> u8{
1793 // cov_flags generated output from elsewhere in this file
1794 extern "C" {
1795 #[no_mangle]
1796 static lower_case: u8;
1797 }
1798
1799 let OtherVar = SOME_VAR + 1;
1800 OtherVar
1801}
1802
1803#[allow(nonstandard_style)]
1804mod CheckNonstandardStyle {
1805 fn HiImABadFnName() {}
1806}
1807
1808#[allow(bad_style)]
1809mod CheckBadStyle {
1810 fn HiImABadFnName() {}
1811}
1812
1813mod F {
1814 #![allow(non_snake_case)]
1815 fn CheckItWorksWithModAttr(BAD_NAME_HI: u8) {}
1816}
1817
1818#[allow(non_snake_case, non_camel_case_types)]
1819pub struct some_type {
1820 SOME_FIELD: u8,
1821 SomeField: u16,
1822}
1823
1824#[allow(non_upper_case_globals)]
1825pub const some_const: u8 = 10;
1826
1827#[allow(non_upper_case_globals)]
1828pub static SomeStatic: u8 = 10;
1829 "#,
1830 );
1831 }
1832
1833 #[test]
1834 fn allow_attributes_crate_attr() {
1835 check_diagnostics(
1836 r#"
1837#![allow(non_snake_case)]
1838
1839mod F {
1840 fn CheckItWorksWithCrateAttr(BAD_NAME_HI: u8) {}
1841}
1842 "#,
1843 );
1844 }
1845
1846 #[test]
1847 #[ignore]
1848 fn bug_trait_inside_fn() {
1849 // FIXME:
1850 // This is broken, and in fact, should not even be looked at by this
1851 // lint in the first place. There's weird stuff going on in the
1852 // collection phase.
1853 // It's currently being brought in by:
1854 // * validate_func on `a` recursing into modules
1855 // * then it finds the trait and then the function while iterating
1856 // through modules
1857 // * then validate_func is called on Dirty
1858 // * ... which then proceeds to look at some unknown module taking no
1859 // attrs from either the impl or the fn a, and then finally to the root
1860 // module
1861 //
1862 // It should find the attribute on the trait, but it *doesn't even see
1863 // the trait* as far as I can tell.
1864
1865 check_diagnostics(
1866 r#"
1867trait T { fn a(); }
1868struct U {}
1869impl T for U {
1870 fn a() {
1871 // this comes out of bitflags, mostly
1872 #[allow(non_snake_case)]
1873 trait __BitFlags {
1874 const HiImAlsoBad: u8 = 2;
1875 #[inline]
1876 fn Dirty(&self) -> bool {
1877 false
1878 }
1879 }
1880
1881 }
1882}
1883 "#,
1884 );
1885 }
1886
1887 #[test]
1888 #[ignore]
1889 fn bug_traits_arent_checked() {
1890 // FIXME: Traits and functions in traits aren't currently checked by
1891 // r-a, even though rustc will complain about them.
1892 check_diagnostics(
1893 r#"
1894trait BAD_TRAIT {
1895 // ^^^^^^^^^ Trait `BAD_TRAIT` should have CamelCase name, e.g. `BadTrait`
1896 fn BAD_FUNCTION();
1897 // ^^^^^^^^^^^^ Function `BAD_FUNCTION` should have snake_case name, e.g. `bad_function`
1898 fn BadFunction();
1899 // ^^^^^^^^^^^^ Function `BadFunction` should have snake_case name, e.g. `bad_function`
1900}
1901 "#,
1902 );
1903 }
1904
1905 #[test]
1906 fn ignores_extern_items() {
1907 cov_mark::check!(extern_func_incorrect_case_ignored);
1908 cov_mark::check!(extern_static_incorrect_case_ignored);
1909 check_diagnostics(
1910 r#"
1911extern {
1912 fn NonSnakeCaseName(SOME_VAR: u8) -> u8;
1913 pub static SomeStatic: u8 = 10;
1914}
1915 "#,
1916 );
1917 }
1918
1919 #[test]
1920 fn infinite_loop_inner_items() {
1921 check_diagnostics(
1922 r#"
1923fn qualify() {
1924 mod foo {
1925 use super::*;
1926 }
1927}
1928 "#,
1929 )
1930 }
1931
1932 #[test] // Issue #8809.
1933 fn parenthesized_parameter() {
1934 check_diagnostics(r#"fn f((O): _) {}"#)
1935 }
1936}
diff --git a/crates/ide/src/diagnostics/fixes.rs b/crates/ide/src/diagnostics/fixes.rs
deleted file mode 100644
index e4bd90c3f..000000000
--- a/crates/ide/src/diagnostics/fixes.rs
+++ /dev/null
@@ -1,25 +0,0 @@
1//! Provides a way to attach fixes to the diagnostics.
2//! The same module also has all curret custom fixes for the diagnostics implemented.
3mod change_case;
4
5use hir::{diagnostics::Diagnostic, Semantics};
6use ide_assists::AssistResolveStrategy;
7use ide_db::RootDatabase;
8
9use crate::Assist;
10
11/// A [Diagnostic] that potentially has some fixes available.
12///
13/// [Diagnostic]: hir::diagnostics::Diagnostic
14pub(crate) trait DiagnosticWithFixes: Diagnostic {
15 /// `resolve` determines if the diagnostic should fill in the `edit` field
16 /// of the assist.
17 ///
18 /// If `resolve` is false, the edit will be computed later, on demand, and
19 /// can be omitted.
20 fn fixes(
21 &self,
22 sema: &Semantics<RootDatabase>,
23 _resolve: &AssistResolveStrategy,
24 ) -> Option<Vec<Assist>>;
25}
diff --git a/crates/ide/src/diagnostics/fixes/change_case.rs b/crates/ide/src/diagnostics/fixes/change_case.rs
deleted file mode 100644
index db1a37cd6..000000000
--- a/crates/ide/src/diagnostics/fixes/change_case.rs
+++ /dev/null
@@ -1,155 +0,0 @@
1use hir::{db::AstDatabase, diagnostics::IncorrectCase, InFile, Semantics};
2use ide_assists::{Assist, AssistResolveStrategy};
3use ide_db::{base_db::FilePosition, RootDatabase};
4use syntax::AstNode;
5
6use crate::{
7 diagnostics::{unresolved_fix, DiagnosticWithFixes},
8 references::rename::rename_with_semantics,
9};
10
11impl DiagnosticWithFixes for IncorrectCase {
12 fn fixes(
13 &self,
14 sema: &Semantics<RootDatabase>,
15 resolve: &AssistResolveStrategy,
16 ) -> Option<Vec<Assist>> {
17 let root = sema.db.parse_or_expand(self.file)?;
18 let name_node = self.ident.to_node(&root);
19
20 let name_node = InFile::new(self.file, name_node.syntax());
21 let frange = name_node.original_file_range(sema.db);
22 let file_position = FilePosition { file_id: frange.file_id, offset: frange.range.start() };
23
24 let label = format!("Rename to {}", self.suggested_text);
25 let mut res = unresolved_fix("change_case", &label, frange.range);
26 if resolve.should_resolve(&res.id) {
27 let source_change = rename_with_semantics(sema, file_position, &self.suggested_text);
28 res.source_change = Some(source_change.ok().unwrap_or_default());
29 }
30
31 Some(vec![res])
32 }
33}
34
35#[cfg(test)]
36mod change_case {
37 use crate::{
38 diagnostics::tests::{check_diagnostics, check_fix},
39 fixture, AssistResolveStrategy, DiagnosticsConfig,
40 };
41
42 #[test]
43 fn test_rename_incorrect_case() {
44 check_fix(
45 r#"
46pub struct test_struct$0 { one: i32 }
47
48pub fn some_fn(val: test_struct) -> test_struct {
49 test_struct { one: val.one + 1 }
50}
51"#,
52 r#"
53pub struct TestStruct { one: i32 }
54
55pub fn some_fn(val: TestStruct) -> TestStruct {
56 TestStruct { one: val.one + 1 }
57}
58"#,
59 );
60
61 check_fix(
62 r#"
63pub fn some_fn(NonSnakeCase$0: u8) -> u8 {
64 NonSnakeCase
65}
66"#,
67 r#"
68pub fn some_fn(non_snake_case: u8) -> u8 {
69 non_snake_case
70}
71"#,
72 );
73
74 check_fix(
75 r#"
76pub fn SomeFn$0(val: u8) -> u8 {
77 if val != 0 { SomeFn(val - 1) } else { val }
78}
79"#,
80 r#"
81pub fn some_fn(val: u8) -> u8 {
82 if val != 0 { some_fn(val - 1) } else { val }
83}
84"#,
85 );
86
87 check_fix(
88 r#"
89fn some_fn() {
90 let whatAWeird_Formatting$0 = 10;
91 another_func(whatAWeird_Formatting);
92}
93"#,
94 r#"
95fn some_fn() {
96 let what_a_weird_formatting = 10;
97 another_func(what_a_weird_formatting);
98}
99"#,
100 );
101 }
102
103 #[test]
104 fn test_uppercase_const_no_diagnostics() {
105 check_diagnostics(
106 r#"
107fn foo() {
108 const ANOTHER_ITEM$0: &str = "some_item";
109}
110"#,
111 );
112 }
113
114 #[test]
115 fn test_rename_incorrect_case_struct_method() {
116 check_fix(
117 r#"
118pub struct TestStruct;
119
120impl TestStruct {
121 pub fn SomeFn$0() -> TestStruct {
122 TestStruct
123 }
124}
125"#,
126 r#"
127pub struct TestStruct;
128
129impl TestStruct {
130 pub fn some_fn() -> TestStruct {
131 TestStruct
132 }
133}
134"#,
135 );
136 }
137
138 #[test]
139 fn test_single_incorrect_case_diagnostic_in_function_name_issue_6970() {
140 let input = r#"fn FOO$0() {}"#;
141 let expected = r#"fn foo() {}"#;
142
143 let (analysis, file_position) = fixture::position(input);
144 let diagnostics = analysis
145 .diagnostics(
146 &DiagnosticsConfig::default(),
147 AssistResolveStrategy::All,
148 file_position.file_id,
149 )
150 .unwrap();
151 assert_eq!(diagnostics.len(), 1);
152
153 check_fix(input, expected);
154 }
155}
diff --git a/crates/ide/src/diagnostics/incorrect_case.rs b/crates/ide/src/diagnostics/incorrect_case.rs
new file mode 100644
index 000000000..832394400
--- /dev/null
+++ b/crates/ide/src/diagnostics/incorrect_case.rs
@@ -0,0 +1,488 @@
1use hir::{db::AstDatabase, InFile};
2use ide_assists::Assist;
3use ide_db::base_db::FilePosition;
4use syntax::AstNode;
5
6use crate::{
7 diagnostics::{unresolved_fix, Diagnostic, DiagnosticsContext},
8 references::rename::rename_with_semantics,
9 Severity,
10};
11
12// Diagnostic: incorrect-ident-case
13//
14// This diagnostic is triggered if an item name doesn't follow https://doc.rust-lang.org/1.0.0/style/style/naming/README.html[Rust naming convention].
15pub(super) fn incorrect_case(ctx: &DiagnosticsContext<'_>, d: &hir::IncorrectCase) -> Diagnostic {
16 Diagnostic::new(
17 "incorrect-ident-case",
18 format!(
19 "{} `{}` should have {} name, e.g. `{}`",
20 d.ident_type, d.ident_text, d.expected_case, d.suggested_text
21 ),
22 ctx.sema.diagnostics_display_range(InFile::new(d.file, d.ident.clone().into())).range,
23 )
24 .severity(Severity::WeakWarning)
25 .with_fixes(fixes(ctx, d))
26}
27
28fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::IncorrectCase) -> Option<Vec<Assist>> {
29 let root = ctx.sema.db.parse_or_expand(d.file)?;
30 let name_node = d.ident.to_node(&root);
31
32 let name_node = InFile::new(d.file, name_node.syntax());
33 let frange = name_node.original_file_range(ctx.sema.db);
34 let file_position = FilePosition { file_id: frange.file_id, offset: frange.range.start() };
35
36 let label = format!("Rename to {}", d.suggested_text);
37 let mut res = unresolved_fix("change_case", &label, frange.range);
38 if ctx.resolve.should_resolve(&res.id) {
39 let source_change = rename_with_semantics(&ctx.sema, file_position, &d.suggested_text);
40 res.source_change = Some(source_change.ok().unwrap_or_default());
41 }
42
43 Some(vec![res])
44}
45
46#[cfg(test)]
47mod change_case {
48 use crate::{
49 diagnostics::tests::{check_diagnostics, check_fix},
50 fixture, AssistResolveStrategy, DiagnosticsConfig,
51 };
52
53 #[test]
54 fn test_rename_incorrect_case() {
55 check_fix(
56 r#"
57pub struct test_struct$0 { one: i32 }
58
59pub fn some_fn(val: test_struct) -> test_struct {
60 test_struct { one: val.one + 1 }
61}
62"#,
63 r#"
64pub struct TestStruct { one: i32 }
65
66pub fn some_fn(val: TestStruct) -> TestStruct {
67 TestStruct { one: val.one + 1 }
68}
69"#,
70 );
71
72 check_fix(
73 r#"
74pub fn some_fn(NonSnakeCase$0: u8) -> u8 {
75 NonSnakeCase
76}
77"#,
78 r#"
79pub fn some_fn(non_snake_case: u8) -> u8 {
80 non_snake_case
81}
82"#,
83 );
84
85 check_fix(
86 r#"
87pub fn SomeFn$0(val: u8) -> u8 {
88 if val != 0 { SomeFn(val - 1) } else { val }
89}
90"#,
91 r#"
92pub fn some_fn(val: u8) -> u8 {
93 if val != 0 { some_fn(val - 1) } else { val }
94}
95"#,
96 );
97
98 check_fix(
99 r#"
100fn some_fn() {
101 let whatAWeird_Formatting$0 = 10;
102 another_func(whatAWeird_Formatting);
103}
104"#,
105 r#"
106fn some_fn() {
107 let what_a_weird_formatting = 10;
108 another_func(what_a_weird_formatting);
109}
110"#,
111 );
112 }
113
114 #[test]
115 fn test_uppercase_const_no_diagnostics() {
116 check_diagnostics(
117 r#"
118fn foo() {
119 const ANOTHER_ITEM$0: &str = "some_item";
120}
121"#,
122 );
123 }
124
125 #[test]
126 fn test_rename_incorrect_case_struct_method() {
127 check_fix(
128 r#"
129pub struct TestStruct;
130
131impl TestStruct {
132 pub fn SomeFn$0() -> TestStruct {
133 TestStruct
134 }
135}
136"#,
137 r#"
138pub struct TestStruct;
139
140impl TestStruct {
141 pub fn some_fn() -> TestStruct {
142 TestStruct
143 }
144}
145"#,
146 );
147 }
148
149 #[test]
150 fn test_single_incorrect_case_diagnostic_in_function_name_issue_6970() {
151 let input = r#"fn FOO$0() {}"#;
152 let expected = r#"fn foo() {}"#;
153
154 let (analysis, file_position) = fixture::position(input);
155 let diagnostics = analysis
156 .diagnostics(
157 &DiagnosticsConfig::default(),
158 AssistResolveStrategy::All,
159 file_position.file_id,
160 )
161 .unwrap();
162 assert_eq!(diagnostics.len(), 1);
163
164 check_fix(input, expected);
165 }
166
167 #[test]
168 fn incorrect_function_name() {
169 check_diagnostics(
170 r#"
171fn NonSnakeCaseName() {}
172// ^^^^^^^^^^^^^^^^ Function `NonSnakeCaseName` should have snake_case name, e.g. `non_snake_case_name`
173"#,
174 );
175 }
176
177 #[test]
178 fn incorrect_function_params() {
179 check_diagnostics(
180 r#"
181fn foo(SomeParam: u8) {}
182 // ^^^^^^^^^ Parameter `SomeParam` should have snake_case name, e.g. `some_param`
183
184fn foo2(ok_param: &str, CAPS_PARAM: u8) {}
185 // ^^^^^^^^^^ Parameter `CAPS_PARAM` should have snake_case name, e.g. `caps_param`
186"#,
187 );
188 }
189
190 #[test]
191 fn incorrect_variable_names() {
192 check_diagnostics(
193 r#"
194fn foo() {
195 let SOME_VALUE = 10;
196 // ^^^^^^^^^^ Variable `SOME_VALUE` should have snake_case name, e.g. `some_value`
197 let AnotherValue = 20;
198 // ^^^^^^^^^^^^ Variable `AnotherValue` should have snake_case name, e.g. `another_value`
199}
200"#,
201 );
202 }
203
204 #[test]
205 fn incorrect_struct_names() {
206 check_diagnostics(
207 r#"
208struct non_camel_case_name {}
209 // ^^^^^^^^^^^^^^^^^^^ Structure `non_camel_case_name` should have CamelCase name, e.g. `NonCamelCaseName`
210
211struct SCREAMING_CASE {}
212 // ^^^^^^^^^^^^^^ Structure `SCREAMING_CASE` should have CamelCase name, e.g. `ScreamingCase`
213"#,
214 );
215 }
216
217 #[test]
218 fn no_diagnostic_for_camel_cased_acronyms_in_struct_name() {
219 check_diagnostics(
220 r#"
221struct AABB {}
222"#,
223 );
224 }
225
226 #[test]
227 fn incorrect_struct_field() {
228 check_diagnostics(
229 r#"
230struct SomeStruct { SomeField: u8 }
231 // ^^^^^^^^^ Field `SomeField` should have snake_case name, e.g. `some_field`
232"#,
233 );
234 }
235
236 #[test]
237 fn incorrect_enum_names() {
238 check_diagnostics(
239 r#"
240enum some_enum { Val(u8) }
241 // ^^^^^^^^^ Enum `some_enum` should have CamelCase name, e.g. `SomeEnum`
242
243enum SOME_ENUM {}
244 // ^^^^^^^^^ Enum `SOME_ENUM` should have CamelCase name, e.g. `SomeEnum`
245"#,
246 );
247 }
248
249 #[test]
250 fn no_diagnostic_for_camel_cased_acronyms_in_enum_name() {
251 check_diagnostics(
252 r#"
253enum AABB {}
254"#,
255 );
256 }
257
258 #[test]
259 fn incorrect_enum_variant_name() {
260 check_diagnostics(
261 r#"
262enum SomeEnum { SOME_VARIANT(u8) }
263 // ^^^^^^^^^^^^ Variant `SOME_VARIANT` should have CamelCase name, e.g. `SomeVariant`
264"#,
265 );
266 }
267
268 #[test]
269 fn incorrect_const_name() {
270 check_diagnostics(
271 r#"
272const some_weird_const: u8 = 10;
273 // ^^^^^^^^^^^^^^^^ Constant `some_weird_const` should have UPPER_SNAKE_CASE name, e.g. `SOME_WEIRD_CONST`
274"#,
275 );
276 }
277
278 #[test]
279 fn incorrect_static_name() {
280 check_diagnostics(
281 r#"
282static some_weird_const: u8 = 10;
283 // ^^^^^^^^^^^^^^^^ Static variable `some_weird_const` should have UPPER_SNAKE_CASE name, e.g. `SOME_WEIRD_CONST`
284"#,
285 );
286 }
287
288 #[test]
289 fn fn_inside_impl_struct() {
290 check_diagnostics(
291 r#"
292struct someStruct;
293 // ^^^^^^^^^^ Structure `someStruct` should have CamelCase name, e.g. `SomeStruct`
294
295impl someStruct {
296 fn SomeFunc(&self) {
297 // ^^^^^^^^ Function `SomeFunc` should have snake_case name, e.g. `some_func`
298 let WHY_VAR_IS_CAPS = 10;
299 // ^^^^^^^^^^^^^^^ Variable `WHY_VAR_IS_CAPS` should have snake_case name, e.g. `why_var_is_caps`
300 }
301}
302"#,
303 );
304 }
305
306 #[test]
307 fn no_diagnostic_for_enum_varinats() {
308 check_diagnostics(
309 r#"
310enum Option { Some, None }
311
312fn main() {
313 match Option::None {
314 None => (),
315 Some => (),
316 }
317}
318"#,
319 );
320 }
321
322 #[test]
323 fn non_let_bind() {
324 check_diagnostics(
325 r#"
326enum Option { Some, None }
327
328fn main() {
329 match Option::None {
330 SOME_VAR @ None => (),
331 // ^^^^^^^^ Variable `SOME_VAR` should have snake_case name, e.g. `some_var`
332 Some => (),
333 }
334}
335"#,
336 );
337 }
338
339 #[test]
340 fn allow_attributes_crate_attr() {
341 check_diagnostics(
342 r#"
343#![allow(non_snake_case)]
344
345mod F {
346 fn CheckItWorksWithCrateAttr(BAD_NAME_HI: u8) {}
347}
348 "#,
349 );
350 }
351
352 #[test]
353 #[ignore]
354 fn bug_trait_inside_fn() {
355 // FIXME:
356 // This is broken, and in fact, should not even be looked at by this
357 // lint in the first place. There's weird stuff going on in the
358 // collection phase.
359 // It's currently being brought in by:
360 // * validate_func on `a` recursing into modules
361 // * then it finds the trait and then the function while iterating
362 // through modules
363 // * then validate_func is called on Dirty
364 // * ... which then proceeds to look at some unknown module taking no
365 // attrs from either the impl or the fn a, and then finally to the root
366 // module
367 //
368 // It should find the attribute on the trait, but it *doesn't even see
369 // the trait* as far as I can tell.
370
371 check_diagnostics(
372 r#"
373trait T { fn a(); }
374struct U {}
375impl T for U {
376 fn a() {
377 // this comes out of bitflags, mostly
378 #[allow(non_snake_case)]
379 trait __BitFlags {
380 const HiImAlsoBad: u8 = 2;
381 #[inline]
382 fn Dirty(&self) -> bool {
383 false
384 }
385 }
386
387 }
388}
389 "#,
390 );
391 }
392
393 #[test]
394 fn infinite_loop_inner_items() {
395 check_diagnostics(
396 r#"
397fn qualify() {
398 mod foo {
399 use super::*;
400 }
401}
402 "#,
403 )
404 }
405
406 #[test] // Issue #8809.
407 fn parenthesized_parameter() {
408 check_diagnostics(r#"fn f((O): _) {}"#)
409 }
410
411 #[test]
412 fn ignores_extern_items() {
413 cov_mark::check!(extern_func_incorrect_case_ignored);
414 cov_mark::check!(extern_static_incorrect_case_ignored);
415 check_diagnostics(
416 r#"
417extern {
418 fn NonSnakeCaseName(SOME_VAR: u8) -> u8;
419 pub static SomeStatic: u8 = 10;
420}
421 "#,
422 );
423 }
424
425 #[test]
426 #[ignore]
427 fn bug_traits_arent_checked() {
428 // FIXME: Traits and functions in traits aren't currently checked by
429 // r-a, even though rustc will complain about them.
430 check_diagnostics(
431 r#"
432trait BAD_TRAIT {
433 // ^^^^^^^^^ Trait `BAD_TRAIT` should have CamelCase name, e.g. `BadTrait`
434 fn BAD_FUNCTION();
435 // ^^^^^^^^^^^^ Function `BAD_FUNCTION` should have snake_case name, e.g. `bad_function`
436 fn BadFunction();
437 // ^^^^^^^^^^^^ Function `BadFunction` should have snake_case name, e.g. `bad_function`
438}
439 "#,
440 );
441 }
442
443 #[test]
444 fn allow_attributes() {
445 check_diagnostics(
446 r#"
447#[allow(non_snake_case)]
448fn NonSnakeCaseName(SOME_VAR: u8) -> u8{
449 // cov_flags generated output from elsewhere in this file
450 extern "C" {
451 #[no_mangle]
452 static lower_case: u8;
453 }
454
455 let OtherVar = SOME_VAR + 1;
456 OtherVar
457}
458
459#[allow(nonstandard_style)]
460mod CheckNonstandardStyle {
461 fn HiImABadFnName() {}
462}
463
464#[allow(bad_style)]
465mod CheckBadStyle {
466 fn HiImABadFnName() {}
467}
468
469mod F {
470 #![allow(non_snake_case)]
471 fn CheckItWorksWithModAttr(BAD_NAME_HI: u8) {}
472}
473
474#[allow(non_snake_case, non_camel_case_types)]
475pub struct some_type {
476 SOME_FIELD: u8,
477 SomeField: u16,
478}
479
480#[allow(non_upper_case_globals)]
481pub const some_const: u8 = 10;
482
483#[allow(non_upper_case_globals)]
484pub static SomeStatic: u8 = 10;
485 "#,
486 );
487 }
488}
diff --git a/crates/ide/src/diagnostics/unlinked_file.rs b/crates/ide/src/diagnostics/unlinked_file.rs
index 51fe0f360..a5b2e3399 100644
--- a/crates/ide/src/diagnostics/unlinked_file.rs
+++ b/crates/ide/src/diagnostics/unlinked_file.rs
@@ -1,11 +1,6 @@
1//! Diagnostic emitted for files that aren't part of any crate. 1//! Diagnostic emitted for files that aren't part of any crate.
2 2
3use hir::{ 3use hir::db::DefDatabase;
4 db::DefDatabase,
5 diagnostics::{Diagnostic, DiagnosticCode},
6 InFile,
7};
8use ide_assists::AssistResolveStrategy;
9use ide_db::{ 4use ide_db::{
10 base_db::{FileId, FileLoader, SourceDatabase, SourceDatabaseExt}, 5 base_db::{FileId, FileLoader, SourceDatabase, SourceDatabaseExt},
11 source_change::SourceChange, 6 source_change::SourceChange,
@@ -13,92 +8,77 @@ use ide_db::{
13}; 8};
14use syntax::{ 9use syntax::{
15 ast::{self, ModuleItemOwner, NameOwner}, 10 ast::{self, ModuleItemOwner, NameOwner},
16 AstNode, SyntaxNodePtr, 11 AstNode, TextRange, TextSize,
17}; 12};
18use text_edit::TextEdit; 13use text_edit::TextEdit;
19 14
20use crate::{ 15use crate::{
21 diagnostics::{fix, fixes::DiagnosticWithFixes}, 16 diagnostics::{fix, DiagnosticsContext},
22 Assist, 17 Assist, Diagnostic,
23}; 18};
24 19
20#[derive(Debug)]
21pub(crate) struct UnlinkedFile {
22 pub(crate) file: FileId,
23}
24
25// Diagnostic: unlinked-file 25// Diagnostic: unlinked-file
26// 26//
27// This diagnostic is shown for files that are not included in any crate, or files that are part of 27// This diagnostic is shown for files that are not included in any crate, or files that are part of
28// crates rust-analyzer failed to discover. The file will not have IDE features available. 28// crates rust-analyzer failed to discover. The file will not have IDE features available.
29#[derive(Debug)] 29pub(super) fn unlinked_file(ctx: &DiagnosticsContext, d: &UnlinkedFile) -> Diagnostic {
30pub(crate) struct UnlinkedFile { 30 // Limit diagnostic to the first few characters in the file. This matches how VS Code
31 pub(crate) file_id: FileId, 31 // renders it with the full span, but on other editors, and is less invasive.
32 pub(crate) node: SyntaxNodePtr, 32 let range = ctx.sema.db.parse(d.file).syntax_node().text_range();
33 // FIXME: This is wrong if one of the first three characters is not ascii: `//Ы`.
34 let range = range.intersect(TextRange::up_to(TextSize::of("..."))).unwrap_or(range);
35
36 Diagnostic::new("unlinked-file", "file not included in module tree", range)
37 .with_fixes(fixes(ctx, d))
33} 38}
34 39
35impl Diagnostic for UnlinkedFile { 40fn fixes(ctx: &DiagnosticsContext, d: &UnlinkedFile) -> Option<Vec<Assist>> {
36 fn code(&self) -> DiagnosticCode { 41 // If there's an existing module that could add `mod` or `pub mod` items to include the unlinked file,
37 DiagnosticCode("unlinked-file") 42 // suggest that as a fix.
38 }
39 43
40 fn message(&self) -> String { 44 let source_root = ctx.sema.db.source_root(ctx.sema.db.file_source_root(d.file));
41 "file not included in module tree".to_string() 45 let our_path = source_root.path_for_file(&d.file)?;
42 } 46 let module_name = our_path.name_and_extension()?.0;
43 47
44 fn display_source(&self) -> InFile<SyntaxNodePtr> { 48 // Candidates to look for:
45 InFile::new(self.file_id.into(), self.node.clone()) 49 // - `mod.rs` in the same folder
46 } 50 // - we also check `main.rs` and `lib.rs`
51 // - `$dir.rs` in the parent folder, where `$dir` is the directory containing `self.file_id`
52 let parent = our_path.parent()?;
53 let mut paths = vec![parent.join("mod.rs")?, parent.join("lib.rs")?, parent.join("main.rs")?];
47 54
48 fn as_any(&self) -> &(dyn std::any::Any + Send + 'static) { 55 // `submod/bla.rs` -> `submod.rs`
49 self 56 if let Some(newmod) = (|| {
57 let name = parent.name_and_extension()?.0;
58 parent.parent()?.join(&format!("{}.rs", name))
59 })() {
60 paths.push(newmod);
50 } 61 }
51}
52 62
53impl DiagnosticWithFixes for UnlinkedFile { 63 for path in &paths {
54 fn fixes( 64 if let Some(parent_id) = source_root.file_for_path(path) {
55 &self, 65 for krate in ctx.sema.db.relevant_crates(*parent_id).iter() {
56 sema: &hir::Semantics<RootDatabase>, 66 let crate_def_map = ctx.sema.db.crate_def_map(*krate);
57 _resolve: &AssistResolveStrategy, 67 for (_, module) in crate_def_map.modules() {
58 ) -> Option<Vec<Assist>> { 68 if module.origin.is_inline() {
59 // If there's an existing module that could add `mod` or `pub mod` items to include the unlinked file, 69 // We don't handle inline `mod parent {}`s, they use different paths.
60 // suggest that as a fix. 70 continue;
61 71 }
62 let source_root = sema.db.source_root(sema.db.file_source_root(self.file_id));
63 let our_path = source_root.path_for_file(&self.file_id)?;
64 let module_name = our_path.name_and_extension()?.0;
65
66 // Candidates to look for:
67 // - `mod.rs` in the same folder
68 // - we also check `main.rs` and `lib.rs`
69 // - `$dir.rs` in the parent folder, where `$dir` is the directory containing `self.file_id`
70 let parent = our_path.parent()?;
71 let mut paths =
72 vec![parent.join("mod.rs")?, parent.join("lib.rs")?, parent.join("main.rs")?];
73
74 // `submod/bla.rs` -> `submod.rs`
75 if let Some(newmod) = (|| {
76 let name = parent.name_and_extension()?.0;
77 parent.parent()?.join(&format!("{}.rs", name))
78 })() {
79 paths.push(newmod);
80 }
81 72
82 for path in &paths { 73 if module.origin.file_id() == Some(*parent_id) {
83 if let Some(parent_id) = source_root.file_for_path(path) { 74 return make_fixes(ctx.sema.db, *parent_id, module_name, d.file);
84 for krate in sema.db.relevant_crates(*parent_id).iter() {
85 let crate_def_map = sema.db.crate_def_map(*krate);
86 for (_, module) in crate_def_map.modules() {
87 if module.origin.is_inline() {
88 // We don't handle inline `mod parent {}`s, they use different paths.
89 continue;
90 }
91
92 if module.origin.file_id() == Some(*parent_id) {
93 return make_fixes(sema.db, *parent_id, module_name, self.file_id);
94 }
95 } 75 }
96 } 76 }
97 } 77 }
98 } 78 }
99
100 None
101 } 79 }
80
81 None
102} 82}
103 83
104fn make_fixes( 84fn make_fixes(
@@ -181,3 +161,144 @@ fn make_fixes(
181 ), 161 ),
182 ]) 162 ])
183} 163}
164
165#[cfg(test)]
166mod tests {
167 use crate::diagnostics::tests::{check_diagnostics, check_fix, check_fixes, check_no_fix};
168
169 #[test]
170 fn unlinked_file_prepend_first_item() {
171 cov_mark::check!(unlinked_file_prepend_before_first_item);
172 // Only tests the first one for `pub mod` since the rest are the same
173 check_fixes(
174 r#"
175//- /main.rs
176fn f() {}
177//- /foo.rs
178$0
179"#,
180 vec![
181 r#"
182mod foo;
183
184fn f() {}
185"#,
186 r#"
187pub mod foo;
188
189fn f() {}
190"#,
191 ],
192 );
193 }
194
195 #[test]
196 fn unlinked_file_append_mod() {
197 cov_mark::check!(unlinked_file_append_to_existing_mods);
198 check_fix(
199 r#"
200//- /main.rs
201//! Comment on top
202
203mod preexisting;
204
205mod preexisting2;
206
207struct S;
208
209mod preexisting_bottom;)
210//- /foo.rs
211$0
212"#,
213 r#"
214//! Comment on top
215
216mod preexisting;
217
218mod preexisting2;
219mod foo;
220
221struct S;
222
223mod preexisting_bottom;)
224"#,
225 );
226 }
227
228 #[test]
229 fn unlinked_file_insert_in_empty_file() {
230 cov_mark::check!(unlinked_file_empty_file);
231 check_fix(
232 r#"
233//- /main.rs
234//- /foo.rs
235$0
236"#,
237 r#"
238mod foo;
239"#,
240 );
241 }
242
243 #[test]
244 fn unlinked_file_old_style_modrs() {
245 check_fix(
246 r#"
247//- /main.rs
248mod submod;
249//- /submod/mod.rs
250// in mod.rs
251//- /submod/foo.rs
252$0
253"#,
254 r#"
255// in mod.rs
256mod foo;
257"#,
258 );
259 }
260
261 #[test]
262 fn unlinked_file_new_style_mod() {
263 check_fix(
264 r#"
265//- /main.rs
266mod submod;
267//- /submod.rs
268//- /submod/foo.rs
269$0
270"#,
271 r#"
272mod foo;
273"#,
274 );
275 }
276
277 #[test]
278 fn unlinked_file_with_cfg_off() {
279 cov_mark::check!(unlinked_file_skip_fix_when_mod_already_exists);
280 check_no_fix(
281 r#"
282//- /main.rs
283#[cfg(never)]
284mod foo;
285
286//- /foo.rs
287$0
288"#,
289 );
290 }
291
292 #[test]
293 fn unlinked_file_with_cfg_on() {
294 check_diagnostics(
295 r#"
296//- /main.rs
297#[cfg(not(never))]
298mod foo;
299
300//- /foo.rs
301"#,
302 );
303 }
304}