diff options
-rw-r--r-- | Cargo.lock | 7 | ||||
-rw-r--r-- | crates/completion/src/completions/attribute.rs | 2 | ||||
-rw-r--r-- | crates/hir_def/src/nameres.rs | 2 | ||||
-rw-r--r-- | crates/hir_expand/src/builtin_macro.rs | 3 | ||||
-rw-r--r-- | crates/proc_macro_srv/src/rustc_server.rs | 43 | ||||
-rw-r--r-- | crates/project_model/src/cargo_workspace.rs | 23 | ||||
-rw-r--r-- | crates/rust-analyzer/Cargo.toml | 1 | ||||
-rw-r--r-- | crates/rust-analyzer/src/diff.rs | 53 | ||||
-rw-r--r-- | crates/rust-analyzer/src/handlers.rs | 23 | ||||
-rw-r--r-- | crates/rust-analyzer/src/lib.rs | 1 | ||||
-rw-r--r-- | crates/rust-analyzer/tests/rust-analyzer/main.rs | 31 | ||||
-rw-r--r-- | crates/ssr/src/matching.rs | 4 | ||||
-rw-r--r-- | xtask/tests/tidy.rs | 2 |
13 files changed, 155 insertions, 40 deletions
diff --git a/Cargo.lock b/Cargo.lock index 9ddbeac47..1aa0c072d 100644 --- a/Cargo.lock +++ b/Cargo.lock | |||
@@ -349,6 +349,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
349 | checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" | 349 | checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" |
350 | 350 | ||
351 | [[package]] | 351 | [[package]] |
352 | name = "dissimilar" | ||
353 | version = "1.0.2" | ||
354 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
355 | checksum = "fc4b29f4b9bb94bf267d57269fd0706d343a160937108e9619fe380645428abb" | ||
356 | |||
357 | [[package]] | ||
352 | name = "drop_bomb" | 358 | name = "drop_bomb" |
353 | version = "0.1.5" | 359 | version = "0.1.5" |
354 | source = "registry+https://github.com/rust-lang/crates.io-index" | 360 | source = "registry+https://github.com/rust-lang/crates.io-index" |
@@ -1343,6 +1349,7 @@ dependencies = [ | |||
1343 | "anyhow", | 1349 | "anyhow", |
1344 | "cfg", | 1350 | "cfg", |
1345 | "crossbeam-channel 0.5.0", | 1351 | "crossbeam-channel 0.5.0", |
1352 | "dissimilar", | ||
1346 | "env_logger", | 1353 | "env_logger", |
1347 | "expect-test", | 1354 | "expect-test", |
1348 | "flycheck", | 1355 | "flycheck", |
diff --git a/crates/completion/src/completions/attribute.rs b/crates/completion/src/completions/attribute.rs index 19ce2482f..8695eed39 100644 --- a/crates/completion/src/completions/attribute.rs +++ b/crates/completion/src/completions/attribute.rs | |||
@@ -234,7 +234,7 @@ fn parse_comma_sep_input(derive_input: ast::TokenTree) -> Result<FxHashSet<Strin | |||
234 | current_derive = String::new(); | 234 | current_derive = String::new(); |
235 | } | 235 | } |
236 | } else { | 236 | } else { |
237 | current_derive.push_str(token.to_string().trim()); | 237 | current_derive.push_str(token.text().trim()); |
238 | } | 238 | } |
239 | } | 239 | } |
240 | 240 | ||
diff --git a/crates/hir_def/src/nameres.rs b/crates/hir_def/src/nameres.rs index 9bf358775..5682e122d 100644 --- a/crates/hir_def/src/nameres.rs +++ b/crates/hir_def/src/nameres.rs | |||
@@ -454,7 +454,7 @@ mod diagnostics { | |||
454 | }); | 454 | }); |
455 | for token in tokens { | 455 | for token in tokens { |
456 | if token.kind() == SyntaxKind::IDENT | 456 | if token.kind() == SyntaxKind::IDENT |
457 | && token.to_string() == *name | 457 | && token.text() == name.as_str() |
458 | { | 458 | { |
459 | precise_location = Some(token.text_range()); | 459 | precise_location = Some(token.text_range()); |
460 | break 'outer; | 460 | break 'outer; |
diff --git a/crates/hir_expand/src/builtin_macro.rs b/crates/hir_expand/src/builtin_macro.rs index 6382521fb..80b60d59f 100644 --- a/crates/hir_expand/src/builtin_macro.rs +++ b/crates/hir_expand/src/builtin_macro.rs | |||
@@ -259,7 +259,8 @@ fn format_args_expand( | |||
259 | } | 259 | } |
260 | for arg in &mut args { | 260 | for arg in &mut args { |
261 | // Remove `key =`. | 261 | // Remove `key =`. |
262 | if matches!(arg.get(1), Some(tt::TokenTree::Leaf(tt::Leaf::Punct(p))) if p.char == '=') { | 262 | if matches!(arg.get(1), Some(tt::TokenTree::Leaf(tt::Leaf::Punct(p))) if p.char == '=' && p.spacing != tt::Spacing::Joint) |
263 | { | ||
263 | arg.drain(..2); | 264 | arg.drain(..2); |
264 | } | 265 | } |
265 | } | 266 | } |
diff --git a/crates/proc_macro_srv/src/rustc_server.rs b/crates/proc_macro_srv/src/rustc_server.rs index 503f4c101..b54aa1f3b 100644 --- a/crates/proc_macro_srv/src/rustc_server.rs +++ b/crates/proc_macro_srv/src/rustc_server.rs | |||
@@ -204,17 +204,18 @@ pub mod token_stream { | |||
204 | let content = subtree | 204 | let content = subtree |
205 | .token_trees | 205 | .token_trees |
206 | .iter() | 206 | .iter() |
207 | .map(|tkn| { | 207 | .fold((String::new(), true), |(last, last_to_joint), tkn| { |
208 | let s = to_text(tkn); | 208 | let s = [last, to_text(tkn)].join(if last_to_joint { "" } else { " " }); |
209 | let mut is_joint = false; | ||
209 | if let tt::TokenTree::Leaf(tt::Leaf::Punct(punct)) = tkn { | 210 | if let tt::TokenTree::Leaf(tt::Leaf::Punct(punct)) = tkn { |
210 | if punct.spacing == tt::Spacing::Alone { | 211 | if punct.spacing == tt::Spacing::Joint { |
211 | return s + " "; | 212 | is_joint = true; |
212 | } | 213 | } |
213 | } | 214 | } |
214 | s | 215 | (s, is_joint) |
215 | }) | 216 | }) |
216 | .collect::<Vec<_>>() | 217 | .0; |
217 | .concat(); | 218 | |
218 | let (open, close) = match subtree.delimiter.map(|it| it.kind) { | 219 | let (open, close) = match subtree.delimiter.map(|it| it.kind) { |
219 | None => ("", ""), | 220 | None => ("", ""), |
220 | Some(tt::DelimiterKind::Brace) => ("{", "}"), | 221 | Some(tt::DelimiterKind::Brace) => ("{", "}"), |
@@ -710,4 +711,32 @@ mod tests { | |||
710 | assert_eq!(srv.character('c').text, "'c'"); | 711 | assert_eq!(srv.character('c').text, "'c'"); |
711 | assert_eq!(srv.byte_string(b"1234586\x88").text, "b\"1234586\\x88\""); | 712 | assert_eq!(srv.byte_string(b"1234586\x88").text, "b\"1234586\\x88\""); |
712 | } | 713 | } |
714 | |||
715 | #[test] | ||
716 | fn test_rustc_server_to_string() { | ||
717 | let s = TokenStream { | ||
718 | subtree: tt::Subtree { | ||
719 | delimiter: None, | ||
720 | token_trees: vec![ | ||
721 | tt::TokenTree::Leaf(tt::Leaf::Ident(tt::Ident { | ||
722 | text: "struct".into(), | ||
723 | id: tt::TokenId::unspecified(), | ||
724 | })), | ||
725 | tt::TokenTree::Leaf(tt::Leaf::Ident(tt::Ident { | ||
726 | text: "T".into(), | ||
727 | id: tt::TokenId::unspecified(), | ||
728 | })), | ||
729 | tt::TokenTree::Subtree(tt::Subtree { | ||
730 | delimiter: Some(tt::Delimiter { | ||
731 | id: tt::TokenId::unspecified(), | ||
732 | kind: tt::DelimiterKind::Brace, | ||
733 | }), | ||
734 | token_trees: vec![], | ||
735 | }), | ||
736 | ], | ||
737 | }, | ||
738 | }; | ||
739 | |||
740 | assert_eq!(s.to_string(), "struct T {}"); | ||
741 | } | ||
713 | } | 742 | } |
diff --git a/crates/project_model/src/cargo_workspace.rs b/crates/project_model/src/cargo_workspace.rs index bb3b6f2ef..1700cb8a7 100644 --- a/crates/project_model/src/cargo_workspace.rs +++ b/crates/project_model/src/cargo_workspace.rs | |||
@@ -1,6 +1,7 @@ | |||
1 | //! FIXME: write short doc here | 1 | //! FIXME: write short doc here |
2 | 2 | ||
3 | use std::{ | 3 | use std::{ |
4 | convert::TryInto, | ||
4 | ffi::OsStr, | 5 | ffi::OsStr, |
5 | ops, | 6 | ops, |
6 | path::{Path, PathBuf}, | 7 | path::{Path, PathBuf}, |
@@ -196,8 +197,23 @@ impl CargoWorkspace { | |||
196 | if let Some(target) = target { | 197 | if let Some(target) = target { |
197 | meta.other_options(vec![String::from("--filter-platform"), target]); | 198 | meta.other_options(vec![String::from("--filter-platform"), target]); |
198 | } | 199 | } |
200 | |||
199 | let mut meta = meta.exec().with_context(|| { | 201 | let mut meta = meta.exec().with_context(|| { |
200 | format!("Failed to run `cargo metadata --manifest-path {}`", cargo_toml.display()) | 202 | let cwd: Option<AbsPathBuf> = |
203 | std::env::current_dir().ok().and_then(|p| p.try_into().ok()); | ||
204 | |||
205 | let workdir = cargo_toml | ||
206 | .parent() | ||
207 | .map(|p| p.to_path_buf()) | ||
208 | .or(cwd) | ||
209 | .map(|dir| dir.to_string_lossy().to_string()) | ||
210 | .unwrap_or_else(|| "<failed to get path>".into()); | ||
211 | |||
212 | format!( | ||
213 | "Failed to run `cargo metadata --manifest-path {}` in `{}`", | ||
214 | cargo_toml.display(), | ||
215 | workdir | ||
216 | ) | ||
201 | })?; | 217 | })?; |
202 | 218 | ||
203 | let mut out_dir_by_id = FxHashMap::default(); | 219 | let mut out_dir_by_id = FxHashMap::default(); |
@@ -334,6 +350,11 @@ pub(crate) fn load_extern_resources( | |||
334 | let mut cmd = Command::new(toolchain::cargo()); | 350 | let mut cmd = Command::new(toolchain::cargo()); |
335 | cmd.args(&["check", "--message-format=json", "--manifest-path"]).arg(cargo_toml); | 351 | cmd.args(&["check", "--message-format=json", "--manifest-path"]).arg(cargo_toml); |
336 | 352 | ||
353 | // --all-targets includes tests, benches and examples in addition to the | ||
354 | // default lib and bins. This is an independent concept from the --targets | ||
355 | // flag below. | ||
356 | cmd.arg("--all-targets"); | ||
357 | |||
337 | if let Some(target) = &cargo_features.target { | 358 | if let Some(target) = &cargo_features.target { |
338 | cmd.args(&["--target", target]); | 359 | cmd.args(&["--target", target]); |
339 | } | 360 | } |
diff --git a/crates/rust-analyzer/Cargo.toml b/crates/rust-analyzer/Cargo.toml index 0a002337b..0a63593fb 100644 --- a/crates/rust-analyzer/Cargo.toml +++ b/crates/rust-analyzer/Cargo.toml | |||
@@ -17,6 +17,7 @@ path = "src/bin/main.rs" | |||
17 | [dependencies] | 17 | [dependencies] |
18 | anyhow = "1.0.26" | 18 | anyhow = "1.0.26" |
19 | crossbeam-channel = "0.5.0" | 19 | crossbeam-channel = "0.5.0" |
20 | dissimilar = "1.0.2" | ||
20 | env_logger = { version = "0.8.1", default-features = false } | 21 | env_logger = { version = "0.8.1", default-features = false } |
21 | itertools = "0.10.0" | 22 | itertools = "0.10.0" |
22 | jod-thread = "0.1.0" | 23 | jod-thread = "0.1.0" |
diff --git a/crates/rust-analyzer/src/diff.rs b/crates/rust-analyzer/src/diff.rs new file mode 100644 index 000000000..231be5807 --- /dev/null +++ b/crates/rust-analyzer/src/diff.rs | |||
@@ -0,0 +1,53 @@ | |||
1 | //! Generate minimal `TextEdit`s from different text versions | ||
2 | use dissimilar::Chunk; | ||
3 | use ide::{TextEdit, TextRange, TextSize}; | ||
4 | |||
5 | pub(crate) fn diff(left: &str, right: &str) -> TextEdit { | ||
6 | let chunks = dissimilar::diff(left, right); | ||
7 | textedit_from_chunks(chunks) | ||
8 | } | ||
9 | |||
10 | fn textedit_from_chunks(chunks: Vec<dissimilar::Chunk>) -> TextEdit { | ||
11 | let mut builder = TextEdit::builder(); | ||
12 | let mut pos = TextSize::default(); | ||
13 | |||
14 | let mut chunks = chunks.into_iter().peekable(); | ||
15 | while let Some(chunk) = chunks.next() { | ||
16 | if let (Chunk::Delete(deleted), Some(&Chunk::Insert(inserted))) = (chunk, chunks.peek()) { | ||
17 | chunks.next().unwrap(); | ||
18 | let deleted_len = TextSize::of(deleted); | ||
19 | builder.replace(TextRange::at(pos, deleted_len), inserted.into()); | ||
20 | pos += deleted_len; | ||
21 | continue; | ||
22 | } | ||
23 | |||
24 | match chunk { | ||
25 | Chunk::Equal(text) => { | ||
26 | pos += TextSize::of(text); | ||
27 | } | ||
28 | Chunk::Delete(deleted) => { | ||
29 | let deleted_len = TextSize::of(deleted); | ||
30 | builder.delete(TextRange::at(pos, deleted_len)); | ||
31 | pos += deleted_len; | ||
32 | } | ||
33 | Chunk::Insert(inserted) => { | ||
34 | builder.insert(pos, inserted.into()); | ||
35 | } | ||
36 | } | ||
37 | } | ||
38 | builder.finish() | ||
39 | } | ||
40 | |||
41 | #[cfg(test)] | ||
42 | mod tests { | ||
43 | use super::*; | ||
44 | |||
45 | #[test] | ||
46 | fn diff_applies() { | ||
47 | let mut original = String::from("fn foo(a:u32){\n}"); | ||
48 | let result = "fn foo(a: u32) {}"; | ||
49 | let edit = diff(&original, result); | ||
50 | edit.apply(&mut original); | ||
51 | assert_eq!(original, result); | ||
52 | } | ||
53 | } | ||
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs index 23f323f55..948cfc17c 100644 --- a/crates/rust-analyzer/src/handlers.rs +++ b/crates/rust-analyzer/src/handlers.rs | |||
@@ -31,6 +31,7 @@ use serde_json::to_value; | |||
31 | use stdx::{format_to, split_once}; | 31 | use stdx::{format_to, split_once}; |
32 | use syntax::{algo, ast, AstNode, TextRange, TextSize}; | 32 | use syntax::{algo, ast, AstNode, TextRange, TextSize}; |
33 | 33 | ||
34 | use crate::diff::diff; | ||
34 | use crate::{ | 35 | use crate::{ |
35 | cargo_target_spec::CargoTargetSpec, | 36 | cargo_target_spec::CargoTargetSpec, |
36 | config::RustfmtConfig, | 37 | config::RustfmtConfig, |
@@ -840,7 +841,7 @@ pub(crate) fn handle_formatting( | |||
840 | let crate_ids = snap.analysis.crate_for(file_id)?; | 841 | let crate_ids = snap.analysis.crate_for(file_id)?; |
841 | 842 | ||
842 | let file_line_index = snap.analysis.file_line_index(file_id)?; | 843 | let file_line_index = snap.analysis.file_line_index(file_id)?; |
843 | let end_position = to_proto::position(&file_line_index, TextSize::of(file.as_str())); | 844 | let file_line_endings = snap.file_line_endings(file_id); |
844 | 845 | ||
845 | let mut rustfmt = match &snap.config.rustfmt { | 846 | let mut rustfmt = match &snap.config.rustfmt { |
846 | RustfmtConfig::Rustfmt { extra_args } => { | 847 | RustfmtConfig::Rustfmt { extra_args } => { |
@@ -861,16 +862,18 @@ pub(crate) fn handle_formatting( | |||
861 | } | 862 | } |
862 | }; | 863 | }; |
863 | 864 | ||
864 | let mut rustfmt = rustfmt.stdin(Stdio::piped()).stdout(Stdio::piped()).spawn()?; | 865 | let mut rustfmt = |
866 | rustfmt.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?; | ||
865 | 867 | ||
866 | rustfmt.stdin.as_mut().unwrap().write_all(file.as_bytes())?; | 868 | rustfmt.stdin.as_mut().unwrap().write_all(file.as_bytes())?; |
867 | 869 | ||
868 | let output = rustfmt.wait_with_output()?; | 870 | let output = rustfmt.wait_with_output()?; |
869 | let captured_stdout = String::from_utf8(output.stdout)?; | 871 | let captured_stdout = String::from_utf8(output.stdout)?; |
872 | let captured_stderr = String::from_utf8(output.stderr).unwrap_or_default(); | ||
870 | 873 | ||
871 | if !output.status.success() { | 874 | if !output.status.success() { |
872 | match output.status.code() { | 875 | match output.status.code() { |
873 | Some(1) => { | 876 | Some(1) if !captured_stderr.contains("not installed") => { |
874 | // While `rustfmt` doesn't have a specific exit code for parse errors this is the | 877 | // While `rustfmt` doesn't have a specific exit code for parse errors this is the |
875 | // likely cause exiting with 1. Most Language Servers swallow parse errors on | 878 | // likely cause exiting with 1. Most Language Servers swallow parse errors on |
876 | // formatting because otherwise an error is surfaced to the user on top of the | 879 | // formatting because otherwise an error is surfaced to the user on top of the |
@@ -886,8 +889,9 @@ pub(crate) fn handle_formatting( | |||
886 | format!( | 889 | format!( |
887 | r#"rustfmt exited with: | 890 | r#"rustfmt exited with: |
888 | Status: {} | 891 | Status: {} |
889 | stdout: {}"#, | 892 | stdout: {} |
890 | output.status, captured_stdout, | 893 | stderr: {}"#, |
894 | output.status, captured_stdout, captured_stderr, | ||
891 | ), | 895 | ), |
892 | ) | 896 | ) |
893 | .into()); | 897 | .into()); |
@@ -899,10 +903,11 @@ pub(crate) fn handle_formatting( | |||
899 | // The document is already formatted correctly -- no edits needed. | 903 | // The document is already formatted correctly -- no edits needed. |
900 | Ok(None) | 904 | Ok(None) |
901 | } else { | 905 | } else { |
902 | Ok(Some(vec![lsp_types::TextEdit { | 906 | Ok(Some(to_proto::text_edit_vec( |
903 | range: Range::new(Position::new(0, 0), end_position), | 907 | &file_line_index, |
904 | new_text: captured_stdout, | 908 | file_line_endings, |
905 | }])) | 909 | diff(&file, &captured_stdout), |
910 | ))) | ||
906 | } | 911 | } |
907 | } | 912 | } |
908 | 913 | ||
diff --git a/crates/rust-analyzer/src/lib.rs b/crates/rust-analyzer/src/lib.rs index d538ad69a..c9494e300 100644 --- a/crates/rust-analyzer/src/lib.rs +++ b/crates/rust-analyzer/src/lib.rs | |||
@@ -34,6 +34,7 @@ mod request_metrics; | |||
34 | mod lsp_utils; | 34 | mod lsp_utils; |
35 | mod thread_pool; | 35 | mod thread_pool; |
36 | mod document; | 36 | mod document; |
37 | mod diff; | ||
37 | pub mod lsp_ext; | 38 | pub mod lsp_ext; |
38 | pub mod config; | 39 | pub mod config; |
39 | 40 | ||
diff --git a/crates/rust-analyzer/tests/rust-analyzer/main.rs b/crates/rust-analyzer/tests/rust-analyzer/main.rs index e51eb2626..84db0856d 100644 --- a/crates/rust-analyzer/tests/rust-analyzer/main.rs +++ b/crates/rust-analyzer/tests/rust-analyzer/main.rs | |||
@@ -190,15 +190,10 @@ pub use std::collections::HashMap; | |||
190 | }, | 190 | }, |
191 | json!([ | 191 | json!([ |
192 | { | 192 | { |
193 | "newText": r#"mod bar; | 193 | "newText": "", |
194 | |||
195 | fn main() {} | ||
196 | |||
197 | pub use std::collections::HashMap; | ||
198 | "#, | ||
199 | "range": { | 194 | "range": { |
200 | "end": { "character": 0, "line": 6 }, | 195 | "end": { "character": 0, "line": 3 }, |
201 | "start": { "character": 0, "line": 0 } | 196 | "start": { "character": 11, "line": 2 } |
202 | } | 197 | } |
203 | } | 198 | } |
204 | ]), | 199 | ]), |
@@ -248,17 +243,17 @@ pub use std::collections::HashMap; | |||
248 | }, | 243 | }, |
249 | json!([ | 244 | json!([ |
250 | { | 245 | { |
251 | "newText": r#"mod bar; | 246 | "newText": "", |
252 | 247 | "range": { | |
253 | async fn test() {} | 248 | "end": { "character": 0, "line": 3 }, |
254 | 249 | "start": { "character": 17, "line": 2 } | |
255 | fn main() {} | 250 | } |
256 | 251 | }, | |
257 | pub use std::collections::HashMap; | 252 | { |
258 | "#, | 253 | "newText": "", |
259 | "range": { | 254 | "range": { |
260 | "end": { "character": 0, "line": 9 }, | 255 | "end": { "character": 0, "line": 6 }, |
261 | "start": { "character": 0, "line": 0 } | 256 | "start": { "character": 11, "line": 5 } |
262 | } | 257 | } |
263 | } | 258 | } |
264 | ]), | 259 | ]), |
diff --git a/crates/ssr/src/matching.rs b/crates/ssr/src/matching.rs index 99b187311..6cf831431 100644 --- a/crates/ssr/src/matching.rs +++ b/crates/ssr/src/matching.rs | |||
@@ -473,7 +473,9 @@ impl<'db, 'sema> Matcher<'db, 'sema> { | |||
473 | } | 473 | } |
474 | SyntaxElement::Node(n) => { | 474 | SyntaxElement::Node(n) => { |
475 | if let Some(first_token) = n.first_token() { | 475 | if let Some(first_token) = n.first_token() { |
476 | if Some(first_token.to_string()) == next_pattern_token { | 476 | if Some(first_token.text().as_str()) |
477 | == next_pattern_token.as_deref() | ||
478 | { | ||
477 | if let Some(SyntaxElement::Node(p)) = pattern.next() { | 479 | if let Some(SyntaxElement::Node(p)) = pattern.next() { |
478 | // We have a subtree that starts with the next token in our pattern. | 480 | // We have a subtree that starts with the next token in our pattern. |
479 | self.attempt_match_token_tree(phase, &p, &n)?; | 481 | self.attempt_match_token_tree(phase, &p, &n)?; |
diff --git a/xtask/tests/tidy.rs b/xtask/tests/tidy.rs index 6bfa922e6..d1ffb70ad 100644 --- a/xtask/tests/tidy.rs +++ b/xtask/tests/tidy.rs | |||
@@ -139,7 +139,7 @@ fn deny_clippy(path: &PathBuf, text: &String) { | |||
139 | return; | 139 | return; |
140 | } | 140 | } |
141 | 141 | ||
142 | if text.contains("[\u{61}llow(clippy") { | 142 | if text.contains("\u{61}llow(clippy") { |
143 | panic!( | 143 | panic!( |
144 | "\n\nallowing lints is forbidden: {}. | 144 | "\n\nallowing lints is forbidden: {}. |
145 | rust-analyzer intentionally doesn't check clippy on CI. | 145 | rust-analyzer intentionally doesn't check clippy on CI. |