diff --git a/src/tools/rust-analyzer/.github/workflows/ci.yaml b/src/tools/rust-analyzer/.github/workflows/ci.yaml index ca9e9719add48..7ea734e7ef1c0 100644 --- a/src/tools/rust-analyzer/.github/workflows/ci.yaml +++ b/src/tools/rust-analyzer/.github/workflows/ci.yaml @@ -41,7 +41,11 @@ jobs: proc-macro-srv: if: github.repository == 'rust-lang/rust-analyzer' name: proc-macro-srv - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest, windows-latest] steps: - name: Checkout repository @@ -206,11 +210,8 @@ jobs: - name: Install Rust toolchain run: | - # FIXME: Pin nightly due to a regression in miri on nightly-2026-02-12. - # See https://github.com/rust-lang/miri/issues/4855. - # Revert to plain `nightly` once this is fixed upstream. - rustup toolchain install nightly-2026-02-10 - rustup default nightly-2026-02-10 + rustup toolchain install nightly + rustup default nightly rustup component add miri # - name: Cache Dependencies diff --git a/src/tools/rust-analyzer/.github/workflows/release.yaml b/src/tools/rust-analyzer/.github/workflows/release.yaml index 7d6e0199abe0b..85ff3d08a78a3 100644 --- a/src/tools/rust-analyzer/.github/workflows/release.yaml +++ b/src/tools/rust-analyzer/.github/workflows/release.yaml @@ -252,9 +252,7 @@ jobs: matrix: include: - cmd: vsce - pat: MARKETPLACE_TOKEN - cmd: ovsx - pat: OPENVSX_TOKEN steps: - name: Install Nodejs uses: actions/setup-node@v6 @@ -277,6 +275,8 @@ jobs: - name: Publish Extension if: github.repository == 'rust-lang/rust-analyzer' + env: + PUBLISH_PAT: ${{ (matrix.cmd == 'vsce' && secrets.MARKETPLACE_TOKEN) || (matrix.cmd == 'ovsx' && secrets.OPENVSX_TOKEN) }} working-directory: ./editors/code - run: npx ${{ matrix.cmd }} publish --skip-duplicate --pat ${{ secrets[matrix.pat] }} --packagePath ../../dist/rust-analyzer-*.vsix ${{ github.ref != 'refs/heads/release' && '--pre-release' || '' }} + run: npx ${{ matrix.cmd }} publish --skip-duplicate --pat "$PUBLISH_PAT" --packagePath ../../dist/rust-analyzer-*.vsix ${{ github.ref != 'refs/heads/release' && '--pre-release' || '' }} timeout-minutes: 2 diff --git a/src/tools/rust-analyzer/AI_POLICY.md b/src/tools/rust-analyzer/AI_POLICY.md index afe7f82a0a11c..59db4a07b4fb5 100644 --- a/src/tools/rust-analyzer/AI_POLICY.md +++ b/src/tools/rust-analyzer/AI_POLICY.md @@ -30,6 +30,14 @@ E-easy issues are usually easier for maintainers to just fix directly than write AI *may* be used to understand the codebase for E-easy+E-has-instructions contributions, but not to write any code. -This policy was adapted from [uv's AI policy]. +When using AI to author changes to *analysis* - the code responsible for analyzing Rust code and not for implementing IDE features, including +but not limited to: type inference, MIR, name resolution, macro expansion - generally anything in the crates `parser`, `mbe`, `hir-expand`, `hir-def`, `hir-ty`, +although there are exceptions; **including when using AI only to analyze bugs and not to write code**, you are required to include a citation +of the rustc code responsible for the change you did, along with an explanation of how your change follows from it in case this is not immediately clear. + +The reason for that is that it is almost impossible to be fully correct in analysis if we implement things differently from rustc. We should not guess +how to fix bugs in analysis without looking at the rustc code. + +This policy was inspired by [uv's AI policy]. [uv's AI policy]: https://github.com/astral-sh/.github/blob/c5187e200db51bfe11d56e13053d29bd3793fdd8/AI_POLICY.md diff --git a/src/tools/rust-analyzer/Cargo.lock b/src/tools/rust-analyzer/Cargo.lock index 983cfaf992dfe..5835ed9e552e7 100644 --- a/src/tools/rust-analyzer/Cargo.lock +++ b/src/tools/rust-analyzer/Cargo.lock @@ -1890,7 +1890,7 @@ dependencies = [ "paths", "proc-macro-test", "span", - "temp-dir", + "stdx", ] [[package]] @@ -1966,7 +1966,6 @@ dependencies = [ "serde_json", "span", "stdx", - "temp-dir", "toml", "toolchain", "tracing", @@ -2752,12 +2751,6 @@ dependencies = [ "tt", ] -[[package]] -name = "temp-dir" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016ef9739649996fcc983b9c588fe3d557cf216d4d98503ce1b057ab5a66d689" - [[package]] name = "tempfile" version = "3.27.0" diff --git a/src/tools/rust-analyzer/Cargo.toml b/src/tools/rust-analyzer/Cargo.toml index b798e364555df..d77e89df45e44 100644 --- a/src/tools/rust-analyzer/Cargo.toml +++ b/src/tools/rust-analyzer/Cargo.toml @@ -4,7 +4,7 @@ exclude = ["crates/proc-macro-srv/proc-macro-test/imp"] resolver = "2" [workspace.package] -rust-version = "1.95" +rust-version = "1.98" edition = "2024" license = "MIT OR Apache-2.0" authors = ["rust-analyzer team"] @@ -143,7 +143,6 @@ smallvec = { version = "1.15.1", features = [ "const_generics", ] } smol_str = "0.3.2" -temp-dir = "0.2.0" text-size = "1.1.1" toml = "1.1.2" tracing = { version = "0.1.41", default-features = false, features = ["std"] } diff --git a/src/tools/rust-analyzer/crates/hir-def/src/attrs/docs.rs b/src/tools/rust-analyzer/crates/hir-def/src/attrs/docs.rs index 3dc278fb1a703..f91140ee8f934 100644 --- a/src/tools/rust-analyzer/crates/hir-def/src/attrs/docs.rs +++ b/src/tools/rust-analyzer/crates/hir-def/src/attrs/docs.rs @@ -31,7 +31,7 @@ use tt::{TextRange, TextSize}; use crate::{macro_call_as_call_id, nameres::MacroSubNs, resolver::Resolver}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub(crate) struct DocsSourceMapLine { +struct DocsSourceMapLine { /// The offset in [`Docs::docs`]. string_offset: TextSize, /// The offset in the AST of the text. `None` for macro-expanded doc strings @@ -62,6 +62,20 @@ pub struct Docs { macro_calls: ThinVec<(AstId, MacroCallId)>, } +#[derive(Clone, Copy)] +enum DocCommentKind { + /// `///` etc.. + Sugared(ast::CommentShape), + /// `#[doc = ""]`. + Desugared, +} + +#[derive(Default)] +struct Indent { + lines: Vec>, + seen_sugared: bool, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum IsInnerDoc { No, @@ -199,51 +213,138 @@ impl Docs { )); } - fn extend_with_doc_comment(&mut self, comment: ast::Comment, indent: &mut usize) { + fn extend_with_doc_comment(&mut self, comment: ast::Comment, indent: &mut Indent) { let Some((doc, offset)) = comment.doc_comment() else { return }; - self.extend_with_doc_str(doc, comment.syntax().text_range().start() + offset, indent); + let offset = comment.syntax().text_range().start() + offset; + self.extend_with_doc_str( + doc, + offset, + DocCommentKind::Sugared(comment.kind().shape), + indent, + ); } - fn extend_with_doc_attr(&mut self, value: ast::String, indent: &mut usize) { + fn extend_with_doc_attr(&mut self, value: ast::String, indent: &mut Indent) { let Some(value_offset) = value.text_range_between_quotes() else { return }; let value_offset = value_offset.start(); let Ok(value) = value.value() else { return }; // FIXME: Handle source maps for escaped text. - self.extend_with_doc_str(&value, value_offset, indent); + self.extend_with_doc_str(&value, value_offset, DocCommentKind::Desugared, indent); } - pub(crate) fn extend_with_doc_str( + fn extend_with_doc_str( &mut self, doc: &str, offset_in_ast: TextSize, - indent: &mut usize, + comment_kind: DocCommentKind, + indent: &mut Indent, ) { - self.push_doc_lines(doc, Some(offset_in_ast), indent); + self.push_doc_lines(doc, Some(offset_in_ast), comment_kind, indent); } - fn extend_with_unmapped_doc_str(&mut self, doc: &str, indent: &mut usize) { - self.push_doc_lines(doc, None, indent); + fn extend_with_unmapped_doc_str(&mut self, doc: &str, indent: &mut Indent) { + self.push_doc_lines(doc, None, DocCommentKind::Desugared, indent); } - fn push_doc_lines(&mut self, doc: &str, mut ast_offset: Option, indent: &mut usize) { - for line in doc.split('\n') { - self.docs_source_map - .push(DocsSourceMapLine { string_offset: TextSize::of(&self.docs), ast_offset }); - if let Some(ref mut offset) = ast_offset { - *offset += TextSize::of(line) + TextSize::of("\n"); + /// Beautifies `doc` and appends the result to `self.docs`, one line at a time via + /// [`Docs::push_doc_line`]. Mirrors rustc's [`beautify_doc_string`], delegating to + /// [`get_vertical_trim`] and [`get_horizontal_trim`] for the multi-line case. + /// + /// Individual `///` line comments always reach us as a single-line `doc`, so the + /// `!doc.contains('\n')` fast path fires and the multi-line logic never runs on them. + /// Desugared `#[doc = "..."]` strings and macro-expanded docs also route through here + /// with `shape = CommentShape::Line`, matching rustc. + /// + /// Unlike rustc's version, which joins the beautified lines into a new interned `Symbol`, + /// this port pushes each line individually via [`Docs::push_doc_line`] and pairs it with + /// its byte offset relative to `doc`'s start so the source-map records accurate per-line + /// offsets. + /// + /// [`beautify_doc_string`]: https://github.com/rust-lang/rust/blob/16a623ad672a92409b5c04beb303583c6cf72a7e/compiler/rustc_ast/src/util/comments.rs#L37 + fn push_doc_lines( + &mut self, + doc: &str, + ast_offset: Option, + comment_kind: DocCommentKind, + indent: &mut Indent, + ) { + // Note: this is pushed even if there are only empty lines here, because that's what rustdoc does. + let shape = match comment_kind { + DocCommentKind::Sugared(shape) => { + indent.seen_sugared = true; + shape } + // rustc uses `Line` for desugared comments. + DocCommentKind::Desugared => ast::CommentShape::Line, + }; + + if !doc.contains('\n') { + self.push_doc_line(doc, ast_offset, comment_kind, indent); + return; + } - let line = line.trim_end(); - if let Some(line_indent) = line.chars().position(|ch| !ch.is_whitespace()) { - // Empty lines are handled because `position()` returns `None` for them. - *indent = std::cmp::min(*indent, line_indent); + let mut lines: Vec<(&str, TextSize)> = doc + .lines() + .map(|line| { + let offset = TextSize::new(doc.substr_range(line).unwrap().start as u32); + (line, offset) + }) + .collect(); + + let raw_lines: Vec<&str> = lines.iter().map(|(l, _)| *l).collect(); + let lines = match get_vertical_trim(&raw_lines) { + Some((i, j)) => &mut lines[i..j], + None => &mut lines[..], + }; + + let raw_lines: Vec<&str> = lines.iter().map(|(l, _)| *l).collect(); + if let Some(horizontal) = get_horizontal_trim(&raw_lines, shape) { + let horizontal_len = TextSize::of(horizontal.as_str()); + // Strip `"[ \t]*\*"` from each line where present, exactly like rustc. + for (line, line_offset) in lines.iter_mut() { + if let Some(rest) = line.strip_prefix(horizontal.as_str()) { + *line = rest; + *line_offset += horizontal_len; + if shape == ast::CommentShape::Block + && (*line == "*" || line.starts_with("* ") || line.starts_with("**")) + { + *line = &line[1..]; + *line_offset += TextSize::of("*"); + } + } } - self.docs.push_str(line); - self.docs.push('\n'); } + + for (line, line_offset) in lines.iter().copied() { + self.push_doc_line(line, ast_offset.map(|it| it + line_offset), comment_kind, indent); + } + } + + /// Appends a single beautified line to `self.docs` and records its source-map row. + fn push_doc_line( + &mut self, + line: &str, + ast_offset: Option, + comment_kind: DocCommentKind, + indent: &mut Indent, + ) { + self.docs_source_map + .push(DocsSourceMapLine { string_offset: TextSize::of(&self.docs), ast_offset }); + + let line = line.trim_end(); + let line_indent = if line.chars().any(|ch| !ch.is_whitespace()) { + // Empty lines are handled because `any()` returns `false` for them. + let line_indent = line.bytes().take_while(|c| *c == b' ' || *c == b'\t').count(); + Some((line_indent, comment_kind)) + } else { + None + }; + indent.lines.push(line_indent); + self.docs.push_str(line); + self.docs.push('\n'); } - fn remove_indent(&mut self, indent: usize, start_source_map_index: usize) { + fn remove_indent(&mut self, indent: &Indent) { /// In case of panics, we want to avoid corrupted UTF-8 in `self.docs`, so we clear it. struct Guard<'a>(&'a mut Docs); impl Drop for Guard<'_> { @@ -271,8 +372,38 @@ impl Docs { return; } + // `add` is used in case the most common sugared doc syntax is used ("/// "). The other + // fragments kind's lines are never starting with a whitespace unless they are using some + // markdown formatting requiring it. Therefore, if the doc block have a mix between the two, + // we need to take into account the fact that the minimum indent minus one (to take this + // whitespace into account). + // + // For example: + // + // /// hello! + // #[doc = "another"] + // + // In this case, you want "hello! another" and not "hello! another". + let add_indent = if indent.seen_sugared { 1 } else { 0 }; + + let Some(min_indent) = indent + .lines + .iter() + .filter_map(|it| *it) + .map(|(line_indent, line_kind)| { + line_indent + + match line_kind { + DocCommentKind::Sugared(_) => 0, + DocCommentKind::Desugared => add_indent, + } + }) + .min() + else { + return; + }; + let guard = Guard(self); - let source_map = &mut guard.0.docs_source_map[start_source_map_index..]; + let source_map = guard.0.docs_source_map.as_mut_slice(); let Some(&DocsSourceMapLine { string_offset: mut copy_into, .. }) = source_map.first() else { return; @@ -289,7 +420,14 @@ impl Docs { let line_docs = &guard.0.docs[TextRange::new(line_source.string_offset, string_end_offset)]; let line_docs_len = TextSize::of(line_docs); - let indent_size = line_docs.char_indices().nth(indent).map_or_else( + let indent_size = if let Some((_, DocCommentKind::Desugared)) = indent.lines[idx] + && min_indent > 0 + { + min_indent - add_indent + } else { + min_indent + }; + let indent_size = line_docs.char_indices().nth(indent_size).map_or_else( || TextSize::of(line_docs) - TextSize::of("\n"), |(offset, _)| TextSize::new(offset as u32), ); @@ -347,6 +485,79 @@ impl Docs { } } +/// Copied verbatim from rustc's [`beautify_doc_string`], modulo `CommentKind`/`CommentShape` +/// renaming. +/// +/// [`beautify_doc_string`]: https://github.com/rust-lang/rust/blob/16a623ad672a92409b5c04beb303583c6cf72a7e/compiler/rustc_ast/src/util/comments.rs#L38 +fn get_vertical_trim(lines: &[&str]) -> Option<(usize, usize)> { + let mut i = 0; + let mut j = lines.len(); + // first line of all-stars should be omitted + if lines.first().is_some_and(|line| line.chars().all(|c| c == '*')) { + i += 1; + } + + // like the first, a last line of all stars should be omitted + if j > i && !lines[j - 1].is_empty() && lines[j - 1].chars().all(|c| c == '*') { + j -= 1; + } + + if i != 0 || j != lines.len() { Some((i, j)) } else { None } +} + +/// Copied verbatim from rustc's [`beautify_doc_string`], modulo `CommentKind`/`CommentShape` +/// renaming and returning `String` rather than interning to `Symbol`. +/// +/// [`beautify_doc_string`]: https://github.com/rust-lang/rust/blob/16a623ad672a92409b5c04beb303583c6cf72a7e/compiler/rustc_ast/src/util/comments.rs#L54 +fn get_horizontal_trim(lines: &[&str], kind: ast::CommentShape) -> Option { + let mut i = usize::MAX; + let mut first = true; + + // In case we have doc comments like `/**` or `/*!`, we want to remove stars if they are + // present. However, we first need to strip the empty lines so they don't get in the middle + // when we try to compute the "horizontal trim". + let lines = match kind { + ast::CommentShape::Block => { + // Whatever happens, we skip the first line. + let mut i = lines + .first() + .map(|l| if l.trim_start().starts_with('*') { 0 } else { 1 }) + .unwrap_or(0); + let mut j = lines.len(); + + while i < j && lines[i].trim().is_empty() { + i += 1; + } + while j > i && lines[j - 1].trim().is_empty() { + j -= 1; + } + &lines[i..j] + } + ast::CommentShape::Line => lines, + }; + + for line in lines { + for (j, c) in line.chars().enumerate() { + if j > i || !"* \t".contains(c) { + return None; + } + if c == '*' { + if first { + i = j; + first = false; + } else if i != j { + return None; + } + break; + } + } + if i >= line.len() { + return None; + } + } + Some(lines.first()?[..i].to_string()) +} + struct DocMacroExpander<'db> { db: &'db dyn SourceDatabase, krate: Crate, @@ -442,7 +653,7 @@ fn extend_with_attrs<'a, 'db>( node: &SyntaxNode, file_id: HirFileId, expect_inner_attrs: bool, - indent: &mut usize, + indent: &mut Indent, get_cfg_options: &dyn Fn() -> &'a CfgOptions, cfg_options: &mut Option<&'a CfgOptions>, make_resolver: &dyn Fn() -> Resolver<'db>, @@ -525,8 +736,8 @@ pub(crate) fn extract_docs<'a, 'db>( let mut cfg_options = None; + let mut indent = Indent::default(); if let Some(outer_mod_decl) = outer_mod_decl { - let mut indent = usize::MAX; // For outer docs (the `mod foo;` declaration), use the module's own resolver. extend_with_attrs( &mut result, @@ -540,12 +751,9 @@ pub(crate) fn extract_docs<'a, 'db>( &mut cfg_options, resolver, ); - result.remove_indent(indent, 0); result.outline_mod = Some((outer_mod_decl.file_id, result.docs_source_map.len())); } - let inline_source_map_start = result.docs_source_map.len(); - let mut indent = usize::MAX; // For inline docs, use the item's own resolver. extend_with_attrs( &mut result, @@ -574,7 +782,7 @@ pub(crate) fn extract_docs<'a, 'db>( resolver, ); } - result.remove_indent(indent, inline_source_map_start); + result.remove_indent(&indent); result.remove_last_newline(); @@ -587,13 +795,14 @@ pub(crate) fn extract_docs<'a, 'db>( mod tests { use expect_test::expect; use hir_expand::InFile; + use syntax::{AstToken, ast}; use test_fixture::WithFixture; use thin_vec::ThinVec; use tt::{TextRange, TextSize}; use crate::test_db::TestDB; - use super::{Docs, IsInnerDoc}; + use super::{DocCommentKind, Docs, Indent, IsInnerDoc}; #[test] fn docs() { @@ -608,12 +817,17 @@ mod tests { outline_inner_docs_start: None, macro_calls: ThinVec::new(), }; - let mut indent = usize::MAX; + let mut indent = Indent::default(); let outer = " foo\n\tbar baz"; let mut ast_offset = TextSize::new(123); for line in outer.split('\n') { - docs.extend_with_doc_str(line, ast_offset, &mut indent); + docs.extend_with_doc_str( + line, + ast_offset, + DocCommentKind::Sugared(ast::CommentShape::Line), + &mut indent, + ); ast_offset += TextSize::of(line) + TextSize::of("\n"); } @@ -621,11 +835,15 @@ mod tests { ast_offset += TextSize::new(123); let inner = " bar \n baz"; for line in inner.split('\n') { - docs.extend_with_doc_str(line, ast_offset, &mut indent); + docs.extend_with_doc_str( + line, + ast_offset, + DocCommentKind::Sugared(ast::CommentShape::Line), + &mut indent, + ); ast_offset += TextSize::of(line) + TextSize::of("\n"); } - assert_eq!(indent, 1); expect![[r#" [ DocsSourceMapLine { @@ -656,7 +874,7 @@ mod tests { "#]] .assert_debug_eq(&docs.docs_source_map); - docs.remove_indent(indent, 0); + docs.remove_indent(&indent); assert_eq!(docs.inline_inner_docs_start, Some(TextSize::new(13))); @@ -762,4 +980,107 @@ mod tests { Some((in_file(range(263, 265)), IsInnerDoc::Yes)) ); } + + #[test] + fn sugared_desugared_mix() { + let (_db, file_id) = TestDB::with_single_file(""); + let mut docs = Docs { + docs: String::new(), + docs_source_map: Vec::new(), + outline_mod: None, + inline_file: file_id.into(), + prefix_len: TextSize::new(0), + inline_inner_docs_start: None, + outline_inner_docs_start: None, + macro_calls: ThinVec::new(), + }; + let mut indent = Indent::default(); + + docs.push_doc_lines( + " hello!", + None, + DocCommentKind::Sugared(ast::CommentShape::Line), + &mut indent, + ); + docs.push_doc_lines("another", None, DocCommentKind::Desugared, &mut indent); + docs.remove_indent(&indent); + docs.remove_last_newline(); + + assert_eq!(docs.docs(), "hello!\nanother"); + } + + /// Extracts the docs of the first comment in `source`, running the same normalization as + /// [`super::extract_docs`] does for inline docs. + fn comment_docs(source: &str) -> Docs { + let (_db, file_id) = TestDB::with_single_file(""); + let comment = syntax::SourceFile::parse(source, span::Edition::CURRENT) + .syntax_node() + .descendants_with_tokens() + .filter_map(|it| it.into_token()) + .find_map(ast::Comment::cast) + .expect("no comment in the fixture"); + let mut docs = Docs { + docs: String::new(), + docs_source_map: Vec::new(), + outline_mod: None, + inline_file: file_id.into(), + prefix_len: TextSize::new(0), + inline_inner_docs_start: None, + outline_inner_docs_start: None, + macro_calls: ThinVec::new(), + }; + let mut indent = Indent::default(); + docs.extend_with_doc_comment(comment, &mut indent); + docs.remove_indent(&indent); + docs.remove_last_newline(); + docs + } + + #[test] + fn block_doc_comment_stars() { + #[track_caller] + fn check(source: &str, expect: expect_test::Expect) { + expect.assert_eq(&comment_docs(source).docs); + } + + // The decoration is stripped, but markdown bullets and `*foo` are content. + // `*bar` doesn't start with `* ` / `**`, so rustc's beautifier only strips the + // horizontal `[ \t]*` prefix (here a single space) and leaves the leading `*` in + // place. That in turn pins the block's minimum indent at 0, so surrounding lines + // aren't re-indented. + check( + "/**\n * foo\n *\n * * bullet\n *bar\n */", + expect![[r#" + foo + + * bullet + *bar + "#]], + ); + // Single-line block doc comments are left alone, like rustdoc does. + check("/** * item */", expect!["* item"]); + // So are blocks without a consistent star column. + check( + "/**\n * foo\n * bar\n */", + expect![[r#" + * foo + * bar + "#]], + ); + } + + #[test] + fn block_doc_comment_source_map() { + let docs = comment_docs("/**\n * foo\n * bar\n */"); + // `.lines()` (matching rustc) doesn't emit a leading empty entry for the newline + // right after `/**`, so the docs body starts at `foo`, not with a blank line. + assert_eq!(docs.docs, "foo\nbar\n"); + + let range = |start, end| TextRange::new(TextSize::new(start), TextSize::new(end)); + let in_file = |range| InFile::new(docs.inline_file, range); + let mapped = |start, end| docs.find_ast_range(range(start, end)); + // Both `foo` and `bar` map back past the stripped ` * ` decoration. + assert_eq!(mapped(0, 3), Some((in_file(range(7, 10)), IsInnerDoc::No))); + assert_eq!(mapped(4, 7), Some((in_file(range(14, 17)), IsInnerDoc::No))); + } } diff --git a/src/tools/rust-analyzer/crates/hir-def/src/expr_store/lower.rs b/src/tools/rust-analyzer/crates/hir-def/src/expr_store/lower.rs index 91faafaf843e0..08cecb52570eb 100644 --- a/src/tools/rust-analyzer/crates/hir-def/src/expr_store/lower.rs +++ b/src/tools/rust-analyzer/crates/hir-def/src/expr_store/lower.rs @@ -135,7 +135,9 @@ pub(super) fn lower_body( } collector.with_expr_root(|collector| { - if let Some(param_list) = parameters { + if let DefWithBodyId::FunctionId(func) = owner + && let Some(param_list) = parameters + { if let Some(self_param_syn) = param_list.self_param().filter(|it| collector.check_cfg(it)) { @@ -155,23 +157,28 @@ pub(super) fn lower_body( Some(collector.expander.in_file(AstPtr::new(&self_param_syn))); } - let is_extern = matches!( - owner, - DefWithBodyId::FunctionId(id) - if matches!(id.loc(db).container, ItemContainerId::ExternBlockId(_)), - ); + let params_are_bare_idents = match func.loc(db).container { + ItemContainerId::ExternBlockId(_) => true, + ItemContainerId::TraitId(_) => body.is_none(), + ItemContainerId::ModuleId(_) | ItemContainerId::ImplId(_) => false, + }; for param in param_list.params() { if collector.check_cfg(¶m) { - let param_pat = if is_extern { - collector.collect_extern_fn_param(param.pat()) - } else { - collector.collect_pat_top(param.pat()) + let param_pat = match param.pat() { + Some(pat) => { + if params_are_bare_idents { + collector.collect_param_as_ident(pat) + } else { + collector.collect_pat_top(Some(pat)) + } + } + None => collector.missing_pat(), }; params.push(Param::new(param_pat)); } } - }; + } collector.collect( &mut self_param, @@ -2792,11 +2799,11 @@ impl<'db> ExprCollector<'db> { } } - fn collect_extern_fn_param(&mut self, pat: Option) -> PatId { - // parameters of functions in `extern` blocks can only be simple identifiers and wildcards. + fn collect_param_as_ident(&mut self, pat: ast::Pat) -> PatId { + // parameters of functions in `extern` blocks and associated trait functions without a body + // can only be simple identifiers and wildcards. // Furthermore, the identifiers in their parameters are always interpreted as bindings, even // if in a normal function they won't be, because they would refer to a path pattern. - let Some(pat) = pat else { return self.missing_pat() }; match &pat { ast::Pat::IdentPat(bp) if bp.is_simple_ident() => { @@ -2812,6 +2819,8 @@ impl<'db> ExprCollector<'db> { pat } ast::Pat::WildcardPat(_) => self.alloc_pat(Pat::Wild, AstPtr::new(&pat)), + ast::Pat::MacroPat(mac) => self + .collect_macro_pat_with(mac.clone(), |this, pat| this.collect_param_as_ident(pat)), _ => { self.store.diagnostics.push(ExpressionStoreDiagnostics::PatternArgInExternFn { node: self.expander.in_file(AstPtr::new(&pat)), @@ -3017,19 +3026,11 @@ impl<'db> ExprCollector<'db> { Pat::Missing } } - ast::Pat::MacroPat(mac) => match mac.macro_call() { - Some(call) => { - let macro_ptr = AstPtr::new(&call); - let src = self.expander.in_file(AstPtr::new(&pat)); - let pat = - self.collect_macro_call(call, macro_ptr, true, |this, expanded_pat| { - this.collect_pat_opt(expanded_pat, binding_list) - }); - self.store.pat_map.insert(src, pat.into()); - return pat; - } - None => Pat::Missing, - }, + ast::Pat::MacroPat(mac) => { + return self.collect_macro_pat_with(mac.clone(), |this, expanded_pat| { + this.collect_pat(expanded_pat, binding_list) + }); + } ast::Pat::RangePat(p) => { let mut range_part_lower = |p: Option| -> Option { p.and_then(|it| { @@ -3068,6 +3069,28 @@ impl<'db> ExprCollector<'db> { self.alloc_pat(pattern, ptr) } + fn collect_macro_pat_with( + &mut self, + mac: ast::MacroPat, + callback: impl FnOnce(&mut Self, ast::Pat) -> PatId, + ) -> PatId { + match mac.macro_call() { + Some(call) => { + let macro_ptr = AstPtr::new(&call); + let src = self.expander.in_file(AstPtr::new(&mac.into())); + let pat = self.collect_macro_call(call, macro_ptr, true, |this, expanded_pat| { + match expanded_pat { + Some(pat) => callback(this, pat), + None => this.missing_pat(), + } + }); + self.store.pat_map.insert(src, pat.into()); + pat + } + None => self.missing_pat(), + } + } + fn collect_pat_opt(&mut self, pat: Option, binding_list: &mut BindingList) -> PatId { match pat { Some(pat) => self.collect_pat(pat, binding_list), @@ -3172,9 +3195,7 @@ impl<'db> ExprCollector<'db> { ) } ast::Pat::MacroPat(pat) => { - let Some(call) = pat.macro_call() else { return self.missing_pat() }; - let ptr = AstPtr::new(&call); - self.collect_macro_call(call, ptr, true, |this, pat| this.collect_ty_pat_opt(pat)) + self.collect_macro_pat_with(pat, |this, pat| this.collect_ty_pat(pat)) } _ => { // FIXME: Emit an error. diff --git a/src/tools/rust-analyzer/crates/hir-def/src/resolver.rs b/src/tools/rust-analyzer/crates/hir-def/src/resolver.rs index 5b11f5ff8bbb6..9d5d80a73ef6a 100644 --- a/src/tools/rust-analyzer/crates/hir-def/src/resolver.rs +++ b/src/tools/rust-analyzer/crates/hir-def/src/resolver.rs @@ -1392,7 +1392,9 @@ impl HasResolver for FunctionId { impl HasResolver for ConstId { fn resolver(self, db: &dyn SourceDatabase) -> Resolver<'_> { - lookup_resolver(db, self) + // Consts can have generic params on nightly. Furthermore they're a `GenericDefId`, + // so not pushing a generic params scope here complicates things (e.g. `TypeOwnerId` tracking). + lookup_resolver(db, self).push_generic_params_scope(db, self.into()) } } diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/diagnostics/unsafe_check.rs b/src/tools/rust-analyzer/crates/hir-ty/src/diagnostics/unsafe_check.rs index 3021de68f3fdf..58598980707b3 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/diagnostics/unsafe_check.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/diagnostics/unsafe_check.rs @@ -315,9 +315,7 @@ impl<'db> UnsafeVisitor<'db> { // https://github.com/rust-lang/rust/pull/129248 // Taking a raw ref to a deref place expr is always safe. Expr::UnaryOp { expr, op: UnaryOp::Deref } => { - self.body - .walk_child_exprs_without_pats(expr, |child| self.walk_expr(child)); - + self.walk_expr(expr); return; } _ => (), diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/display.rs b/src/tools/rust-analyzer/crates/hir-ty/src/display.rs index 9bb0e5b66beeb..dfa088830543f 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/display.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/display.rs @@ -1709,11 +1709,13 @@ impl<'db> HirDisplay<'db> for Ty<'db> { write!(f, "?c.{}", ty.var.as_usize())? } TyKind::Dynamic(bounds, region) => { + let self_ty = interner.default_types().types.dyn_trait_dummy_self; + // We want to put auto traits after principal traits, regardless of their written order. let mut bounds_to_display = SmallVec::<[_; 4]>::new(); let mut auto_trait_bounds = SmallVec::<[_; 4]>::new(); for bound in bounds.iter() { - let clause = bound.with_self_ty(interner, *self); + let clause = bound.with_self_ty(interner, self_ty); match bound.skip_binder() { ExistentialPredicate::Trait(_) | ExistentialPredicate::Projection(_) => { bounds_to_display.push(clause); @@ -1725,13 +1727,13 @@ impl<'db> HirDisplay<'db> for Ty<'db> { if f.render_region(region) { bounds_to_display - .push(rustc_type_ir::OutlivesPredicate(*self, region).upcast(interner)); + .push(rustc_type_ir::OutlivesPredicate(self_ty, region).upcast(interner)); } write_bounds_like_dyn_trait_with_prefix( f, "dyn", - Either::Left(*self), + Either::Left(self_ty), &bounds_to_display, SizedByDefault::NotSized, trait_bounds_need_parens, @@ -1974,12 +1976,12 @@ impl<'db> HirDisplay<'db> for PolyFnSig<'db> { if let Safety::Unsafe = fn_sig_kind.safety() { write!(f, "unsafe ")?; } - // FIXME: Enable this when the FIXME on FnAbi regarding PartialEq is fixed. - // if !matches!(abi, FnAbi::Rust) { - // f.write_str("extern \"")?; - // f.write_str(abi.as_str())?; - // f.write_str("\" ")?; - // } + let abi = self.abi(); + if !matches!(abi, ExternAbi::Rust) { + f.write_str("extern \"")?; + f.write_str(abi.as_str())?; + f.write_str("\" ")?; + } write!(f, "fn(")?; f.write_joined(inputs_and_output.inputs(), ", ")?; if fn_sig_kind.c_variadic() { diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/infer.rs b/src/tools/rust-analyzer/crates/hir-ty/src/infer.rs index 70539cf83673a..3fbb02aee94bd 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/infer.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/infer.rs @@ -1629,10 +1629,6 @@ impl<'db> InferenceContext<'db> { self.defined_anon_consts.borrow_mut().append(&mut defined_anon_consts); } - // FIXME: This function should be private in module. It is currently only used in the consteval, since we need - // `InferenceResult` in the middle of inference. See the fixme comment in `consteval::eval_to_const`. If you - // used this function for another workaround, mention it here. If you really need this function and believe that - // there is no problem in it being `pub(crate)`, remove this comment. fn resolve_all(self) -> InferenceResult<'db> { let InferenceContext { table, diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/infer/expr.rs b/src/tools/rust-analyzer/crates/hir-ty/src/infer/expr.rs index f247b517c541f..fc53d64a2f984 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/infer/expr.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/infer/expr.rs @@ -1329,7 +1329,11 @@ impl<'db> InferenceContext<'db> { } None => { let ty = self.table.next_ty_var(element.into()); - self.infer_expr(element, &Expectation::has_type(ty), ExprIsRead::Yes); + self.infer_expr_suptype_coerce_never( + element, + &Expectation::has_type(ty), + ExprIsRead::Yes, + ); ty } }; diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/lower.rs b/src/tools/rust-analyzer/crates/hir-ty/src/lower.rs index 5f7e6782fdd28..7733b49d32f78 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/lower.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/lower.rs @@ -788,7 +788,7 @@ impl<'db, 'a> TyLoweringContext<'db, 'a> { ) } - /// This is only for `generic_predicates_for_param`, where we can't just + /// This is only for [`resolve_type_param_assoc_type_shorthand`], where we can't just /// lower the self types of the predicates since that could lead to cycles. /// So we just check here if the `type_ref` resolves to a generic param, and which. fn lower_ty_only_param(&self, type_ref: TypeRefId) -> Option { diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/mir/lower/pattern_matching.rs b/src/tools/rust-analyzer/crates/hir-ty/src/mir/lower/pattern_matching.rs index bd1ad70fe6a14..44f410408a40a 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/mir/lower/pattern_matching.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/mir/lower/pattern_matching.rs @@ -491,6 +491,12 @@ impl<'db> MirLowerCtx<'_, 'db> { )? } Pat::Ref { pat, mutability: _ } => { + let ty = cond_place.ty(&self.result, &self.infcx, self.env).ty; + if !ty.is_ref() { + return Err(MirLowerError::TypeError( + "non reference type matched with reference pattern", + )); + } let cond_place = cond_place.project(ProjectionElem::Deref); self.pattern_match_inner(current, current_else, cond_place, *pat, mode)? } diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/mir/lower/tests.rs b/src/tools/rust-analyzer/crates/hir-ty/src/mir/lower/tests.rs index 1f6aa5c926b15..e4f3b61be4556 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/mir/lower/tests.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/mir/lower/tests.rs @@ -49,3 +49,19 @@ fn foo() { "#, ); } + +#[test] +fn ref_pattern_on_unresolved_alias() { + lower_mir( + r#" +//- minicore: sized +trait Tr { + type A; +} + +fn f(x: T::A) { + let &() = x; +} +"#, + ); +} diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/tests/display_source_code.rs b/src/tools/rust-analyzer/crates/hir-ty/src/tests/display_source_code.rs index fe7327134903e..efbb49b0eeb81 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/tests/display_source_code.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/tests/display_source_code.rs @@ -90,6 +90,19 @@ fn foo(foo: &dyn for<'a> Foo<'a>) {} ); } +#[test] +fn render_dyn_ty_under_enclosing_binder() { + check_types_source_code( + r#" +//- minicore: fn +fn test(f: impl for<'b> Fn(&dyn Fn() -> &'b u8)) { + f; + //^ impl Fn(&(dyn Fn() -> &u8 + 'static)) +} +"#, + ); +} + #[test] fn sized_bounds_apit() { check_types_source_code( diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/tests/never_type.rs b/src/tools/rust-analyzer/crates/hir-ty/src/tests/never_type.rs index 1c5f8aa110463..e53f7503dac6b 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/tests/never_type.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/tests/never_type.rs @@ -114,6 +114,21 @@ fn test() { ); } +#[test] +fn array_repeat_never_can_be_reinferred() { + check_no_mismatches( + r#" +fn test() { + let y = [return; 2]; + match y { + [(1, _), (_, false)] => {} + [_, _] => {} + } +} +"#, + ); +} + #[test] fn match_no_arm() { check_types( diff --git a/src/tools/rust-analyzer/crates/hir-ty/src/tests/regression.rs b/src/tools/rust-analyzer/crates/hir-ty/src/tests/regression.rs index 3ed2eb2594c56..ff0e075ff6d69 100644 --- a/src/tools/rust-analyzer/crates/hir-ty/src/tests/regression.rs +++ b/src/tools/rust-analyzer/crates/hir-ty/src/tests/regression.rs @@ -2797,10 +2797,48 @@ where fn extern_fns_cannot_have_param_patterns() { check_no_mismatches( r#" -pub(crate) struct Builder<'a>(&'a ()); +macro_rules! m { + () => { Builder }; +} + +pub(crate) struct Builder; + +unsafe extern "C" { + pub(crate) fn foo(Builder: (), m!(): ()); +} + "#, + ); +} + +#[test] +fn trait_assoc_fns_cannot_have_param_patterns() { + check_no_mismatches( + r#" +macro_rules! m { + () => { Builder }; +} -unsafe extern "C" { - pub(crate) fn foo<'a>(Builder: &Builder<'a>); +pub(crate) struct Builder; + +trait Trait { + fn foo(Builder: (), m!(): ()); +} + "#, + ); + // But assoc fns with bodies do have patterns: + check( + r#" +macro_rules! m { + () => { Builder }; +} + +pub(crate) struct Builder; + +trait Trait { + fn foo(Builder: (), + // ^^^^^^^ expected (), got Builder + m!(): ()) {} + // ^^ expected (), got Builder } "#, ); diff --git a/src/tools/rust-analyzer/crates/hir/src/lib.rs b/src/tools/rust-analyzer/crates/hir/src/lib.rs index 8f747e397c873..9238cdcb3ef85 100644 --- a/src/tools/rust-analyzer/crates/hir/src/lib.rs +++ b/src/tools/rust-analyzer/crates/hir/src/lib.rs @@ -3956,21 +3956,9 @@ impl<'db> GenericSubstitution<'db> { TypeOrConstParamData::TypeParamData(param) => Some(param.name.clone()), TypeOrConstParamData::ConstParamData(_) => None, }); - let parent_len = self.subst.len() - - generics - .iter_type_or_consts() - .filter(|g| matches!(g.1, TypeOrConstParamData::TypeParamData(..))) - .count(); - let container_params = self.subst.as_slice()[..parent_len] - .iter() - .filter_map(|param| param.ty()) - .zip(container_type_params.into_iter().flatten()); - let self_params = self.subst.as_slice()[parent_len..] - .iter() - .filter_map(|param| param.ty()) - .zip(type_params); - container_params - .chain(self_params) + self.subst + .types() + .zip(container_type_params.into_iter().flatten().chain(type_params)) .filter_map(|(ty, name)| { Some(( name?.symbol().clone(), diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/add_return_type.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/add_return_type.rs index e7203a96bb218..453a1b26e08fd 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/add_return_type.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/add_return_type.rs @@ -161,7 +161,7 @@ fn extract_tail(ctx: &AssistContext<'_, '_>) -> Option<(FnType, ast::Expr, Inser let stmt_list = body.stmt_list()?; let tail_expr = stmt_list.tail_expr()?; - let ret_range_end = stmt_list.l_curly_token()?.text_range().start(); + let ret_range_end = stmt_list.l_curly_token()?.text_range().end(); let ret_range = TextRange::new(rparen_pos, ret_range_end); (FnType::Function, tail_expr, ret_range, action) } @@ -215,7 +215,7 @@ mod tests { #[test] fn infer_return_type_cursor_at_return_type_pos() { - cov_mark::check!(cursor_in_ret_position); + cov_mark::check_count!(cursor_in_ret_position, 3); check_assist( add_return_type, r#"fn foo() $0{ @@ -223,6 +223,24 @@ mod tests { }"#, r#"fn foo() -> i32 { 45 +}"#, + ); + check_assist( + add_return_type, + r#"fn foo()$0 { + 45 +}"#, + r#"fn foo() -> i32 { + 45 +}"#, + ); + check_assist( + add_return_type, + r#"fn foo() {$0 + 45 +}"#, + r#"fn foo() -> i32 { + 45 }"#, ); } diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs index c2c50b16de76f..e514e8be4233a 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs @@ -76,10 +76,16 @@ pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext<'_, '_>) - if let Some(t) = ctx.token_at_offset().find(|it| it.kind() == T![;]) { t.parent().and_then(ast::ExprStmt::cast)?.syntax().clone() } else { - let expr = ancestors_at_offset(ctx.source_file().syntax(), ctx.offset()) - .next() - .and_then(ast::Expr::cast)?; - expr.syntax().ancestors().find_map(valid_target_expr(ctx))?.syntax().clone() + // Offer the assist only if the nearest syntax node is an expression, or a record + // field, or a record field’s name. This prevents the assist from appearing when + // it is unlikely to be relevant, such as when the cursor is in a pattern. + // (If we did not want to restrict it this way, we could just apply + // `valid_target_expr()` to all ancestors.) + let expr_or_field = ancestors_at_offset(ctx.source_file().syntax(), ctx.offset()) + .find(|it| !ast::NameRef::can_cast(it.kind())) + .and_then(either::Either::::cast)?; + + expr_or_field.syntax().ancestors().find_map(valid_target_expr(ctx))?.syntax().clone() } } else { match ctx.covering_element() { @@ -95,7 +101,7 @@ pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext<'_, '_>) - let node = node.ancestors().take_while(|anc| anc.text_range() == node.text_range()).last()?; let range = node.text_range(); - let (to_replace, analysis, use_source_expr) = if node.kind() == SyntaxKind::TOKEN_TREE { + let (to_replace, analysis, source_to_extract) = if node.kind() == SyntaxKind::TOKEN_TREE { let (first, last) = extract_token_range_of(&node, ctx.selection_trimmed())?; let first_descend = ctx.sema.descend_into_macros_single_exact(first.clone()); @@ -114,14 +120,16 @@ pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext<'_, '_>) - if !node.text_range().contains_range(original_range.range) { return None; } - (cover_edit_range(&node, original_range.range), expr, true) + let to_replace = cover_edit_range(&node, original_range.range); + let source_to_extract = source_expr(ctx, to_replace.clone())?; + (to_replace, expr, Some(source_to_extract)) } else { let expr = node .descendants() .take_while(|it| range.contains_range(it.text_range())) .find_map(valid_target_expr(ctx))?; let to_extract = expr.syntax().syntax_element(); - (to_extract.clone()..=to_extract, expr, false) + (to_extract.clone()..=to_extract, expr, None) }; let place = match to_replace.start() { NodeOrToken::Node(node) => node.clone(), @@ -220,10 +228,9 @@ pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext<'_, '_>) - editor.add_annotation(pat_name.syntax().clone(), tabstop); } - let to_extract_no_ref = if use_source_expr { - source_expr(ctx, to_replace.clone()).unwrap() - } else { - to_extract_no_ref.clone() + let to_extract_no_ref = match &source_to_extract { + Some(expr) => expr.clone(), + None => to_extract_no_ref.clone(), }; let initializer = match ty.as_ref().filter(|_| needs_ref) { Some(receiver_type) if receiver_type.is_mutable_reference() => { @@ -367,6 +374,11 @@ fn valid_target_expr(ctx: &AssistContext<'_, '_>) -> impl Fn(SyntaxNode) -> Opti let path_resolution = ctx.sema.resolve_path(&path_expr.path()?)?; like_const_value(ctx, path_resolution).then_some(path_expr.into()) } + SyntaxKind::RECORD_EXPR_FIELD => { + // If we are on `k` in `Struct { k: v }`, then extract `v`. + let record_field = ast::RecordExprField::cast(node)?; + record_field.expr() + } _ => ast::Expr::cast(node), } } @@ -952,6 +964,16 @@ fn foo() { check_assist_not_applicable(extract_variable, r#"fn main() { 1 + /* $0comment$0 */ 1; }"#); } + #[test] + fn dont_extract_in_pattern_with_selection() { + check_assist_not_applicable(extract_variable, r#"fn foo() { [].map(|$0bar$0| bar + 1) } "#); + } + + #[test] + fn dont_extract_in_pattern_without_selection() { + check_assist_not_applicable(extract_variable, r#"fn foo() { [].map(|b$0ar| bar + 1) } "#); + } + #[test] fn extract_var_expr_stmt() { cov_mark::check!(test_extract_var_expr_stmt); @@ -1383,6 +1405,46 @@ fn main() { ); } + #[test] + fn extract_var_in_macro_call_with_multiple_args() { + check_assist_not_applicable( + extract_variable, + r#" +macro_rules! m { + ($a:expr, $b:expr) => { $a + $b }; +} +fn f(x: u32) -> u32 { + m!($0x$0, 1) +} +"#, + ); + } + + #[test] + fn extract_var_in_macro_call_with_single_arg() { + check_assist_by_label( + extract_variable, + r#" +macro_rules! m { + ($e:expr) => { $e + 1 }; +} +fn f(x: u32) -> u32 { + m!($0x$0) +} +"#, + r#" +macro_rules! m { + ($e:expr) => { $e + 1 }; +} +fn f(x: u32) -> u32 { + let $0var_name = x; + m!(var_name) +} +"#, + "Extract into variable", + ); + } + #[test] fn extract_var_path_simple() { check_assist_by_label( @@ -1586,6 +1648,87 @@ struct S { foo: i32 } +fn main() { + let $0foo = 1 + 1; + S { foo } +} +"#, + "Extract into variable", + ) + } + + #[test] + fn extract_var_from_record_field() { + check_assist_by_label( + extract_variable, + r#" +struct S { + foo: i32 +} + +fn main() { + S { $0foo: 1 + 1,$0 } +} +"#, + r#" +struct S { + foo: i32 +} + +fn main() { + let $0foo = 1 + 1; + S { foo, } +} +"#, + "Extract into variable", + ) + } + + #[test] + fn extract_var_from_record_field_name() { + check_assist_by_label( + extract_variable, + r#" +struct S { + foo: i32 +} + +fn main() { + S { f$0oo: 1 + 1 } +} +"#, + r#" +struct S { + foo: i32 +} + +fn main() { + let $0foo = 1 + 1; + S { foo } +} +"#, + "Extract into variable", + ) + } + + #[test] + fn extract_var_from_record_field_colon() { + check_assist_by_label( + extract_variable, + r#" +struct S { + foo: i32 +} + +fn main() { + S { foo $0: 1 + 1 } +} +"#, + r#" +struct S { + foo: i32 +} + fn main() { let $0foo = 1 + 1; S { foo } diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/completions.rs b/src/tools/rust-analyzer/crates/ide-completion/src/completions.rs index f1a34f15d0a5b..eec35cc4024e5 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/completions.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/completions.rs @@ -752,7 +752,7 @@ pub(super) fn complete_name_ref<'db>( TypeLocation::TypeAscription(ascription) => { if let TypeAscriptionTarget::RetType { item: Some(item), .. } = ascription - && path_ctx.required_thin_arrow().is_some() + && path_ctx.required_thin_arrow(&ctx.sema).is_some() && matches!(path_ctx.qualified, Qualified::No) { keyword::complete_for_and_where(acc, ctx, &item.clone().into()); diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/context.rs b/src/tools/rust-analyzer/crates/ide-completion/src/context.rs index 705305f557e9c..5935e16542e4a 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/context.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/context.rs @@ -103,7 +103,10 @@ impl PathCompletionCtx<'_> { ) } - pub(crate) fn required_thin_arrow(&self) -> Option<(&'static str, TextSize)> { + pub(crate) fn required_thin_arrow( + &self, + sema: &Semantics<'_, RootDatabase>, + ) -> Option<(&'static str, TextSize)> { let PathKind::Type { location: TypeLocation::TypeAscription(TypeAscriptionTarget::RetType { @@ -117,10 +120,11 @@ impl PathCompletionCtx<'_> { if fn_item.ret_type().is_some_and(|it| it.thin_arrow_token().is_some()) { return None; } + let unmap = |node: &_| sema.original_range_opt(node).map(|it| it.range); let ret_type = fn_item.ret_type().and_then(|it| it.ty()); match (ret_type, fn_item.param_list()) { - (Some(ty), _) => Some(("-> ", ty.syntax().text_range().start())), - (None, Some(param)) => Some((" ->", param.syntax().text_range().end())), + (Some(ty), _) => Some(("-> ", unmap(ty.syntax())?.start())), + (None, Some(param)) => Some((" ->", unmap(param.syntax())?.end())), (None, None) => None, } } diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/render.rs b/src/tools/rust-analyzer/crates/ide-completion/src/render.rs index 43b2a53a7f7ea..de0a9a9174314 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/render.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/render.rs @@ -651,7 +651,7 @@ fn adds_ret_type_arrow( item: &mut Builder, insert_text: String, ) { - if let Some((arrow, at)) = path_ctx.required_thin_arrow() { + if let Some((arrow, at)) = path_ctx.required_thin_arrow(&ctx.sema) { let mut edit = TextEdit::builder(); edit.insert(at, arrow.to_owned()); diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs b/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs index 0e558cf6a2b57..0a4057df974e2 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/tests/expression.rs @@ -4208,3 +4208,38 @@ fn foo(t: T) { "#]], ); } + +#[test] +fn const_is_type_owner() { + check( + r#" +pub struct Boo; +pub struct A(Boo); +impl A { + const X: A = A(B$0); +} + "#, + expect![[r#" + sp Self A + st A A + st Boo Boo + st Boo Boo + bt u32 u32 + kw const + kw crate:: + kw false + kw for + kw if + kw if let + kw loop + kw match + kw self:: + kw true + kw unsafe + kw while + kw while let + ex A::X.0 + ex Boo + "#]], + ); +} diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/tests/type_pos.rs b/src/tools/rust-analyzer/crates/ide-completion/src/tests/type_pos.rs index ad058901c0473..419b15ed868b3 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/tests/type_pos.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/tests/type_pos.rs @@ -244,6 +244,40 @@ fn foo() -> foo::Num "#, ); + check_edit( + "u32", + r#" +macro_rules! identity { ($($t:tt)*) => {$($t)*}; } +identity! { + fn foo() u$0 +} +"#, + r#" +macro_rules! identity { ($($t:tt)*) => {$($t)*}; } +identity! { + fn foo() -> u32 +} +"#, + ); + + check_edit( + "Num", + r#" +macro_rules! identity { ($($t:tt)*) => {$($t)*}; } +mod foo { pub type Num = u32; } +identity! { + fn foo() foo::N$0 +} +"#, + r#" +macro_rules! identity { ($($t:tt)*) => {$($t)*}; } +mod foo { pub type Num = u32; } +identity! { + fn foo() -> foo::Num +} +"#, + ); + // no spaces, test edit order check_edit( "foo", diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/invalid_cast.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/invalid_cast.rs index e1c2053289b96..87647a84cc7ce 100644 --- a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/invalid_cast.rs +++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/invalid_cast.rs @@ -230,7 +230,7 @@ fn foo(_x: isize) { } fn main() { let v: u64 = 5; let x = foo as extern "C" fn() -> isize; - //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: non-primitive cast: `fn foo(isize)` as `fn() -> isize` + //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: non-primitive cast: `fn foo(isize)` as `extern "C" fn() -> isize` let y = v as extern "Rust" fn(isize) -> (isize, isize); //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: non-primitive cast: `u64` as `fn(isize) -> (isize, isize)` y(x()); diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_unsafe.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_unsafe.rs index 18859c0db1e60..4b55995a058df 100644 --- a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_unsafe.rs +++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_unsafe.rs @@ -1061,6 +1061,17 @@ fn foo() {} #[target_feature(enable = "avx2", enable = "fma")] fn bar() { foo(); +} + "#, + ); + } + + #[test] + fn raw_ref_deref_raw_ref_deref() { + check_diagnostics( + r#" +fn foo() { + &raw const *&raw const *&raw const *&2; } "#, ); diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_field.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_field.rs index 682a8130a8822..26f5e45ea3420 100644 --- a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_field.rs +++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_field.rs @@ -85,6 +85,7 @@ fn field_fix(ctx: &DiagnosticsContext<'_, '_>, d: &hir::UnresolvedField<'_>) -> if !is_editable_crate(target_module.krate(ctx.sema.db), ctx.sema.db) || SyntaxKind::from_keyword(field_name, ctx.edition).is_some() + || !syntax::utils::is_identifier(field_name, ctx.edition) { return None; } @@ -148,11 +149,7 @@ fn add_field_to_struct_fix( Some(make::visibility_pub_crate()) }; - let field_name = match field_name.chars().next() { - Some(ch) if ch.is_numeric() => return None, - Some(_) => make::name(field_name), - None => return None, - }; + let field_name = make::name(field_name); let (offset, record_field) = record_field_layout( visibility, @@ -180,12 +177,7 @@ fn add_field_to_struct_fix( // Add a field list to the Unit Struct let mut src_change_builder = SourceChangeBuilder::new(struct_range.file_id.file_id(ctx.sema.db)); - let field_name = match field_name.chars().next() { - // FIXME : See match arm below regarding tuple structs. - Some(ch) if ch.is_numeric() => return None, - Some(_) => make::name(field_name), - None => return None, - }; + let field_name = make::name(field_name); let visibility = if error_range.file_id == struct_range.file_id { None } else { @@ -524,6 +516,18 @@ fn main() {} ) } + #[test] + fn no_fix_when_indexed_on_union() { + check_no_fix( + r#" +union U { a: u32 } +fn main(u: U) { + u.0$0; +} +"#, + ) + } + #[test] fn no_fix_when_without_field() { check_no_fix( diff --git a/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs b/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs index fad322aa4f5da..66ccd1924639e 100644 --- a/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs +++ b/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs @@ -2626,7 +2626,7 @@ fn bar() { fo$0o(); } --- - \<- ` ` here +  \<- ` ` here "#]], ); } @@ -5085,6 +5085,33 @@ fn foo$0() {} ); } +#[test] +fn hover_doc_block_style_leading_asterisks() { + check( + r#" +/** + * Some docs, *not a bullet*. + */ +fn foo$0() {} +"#, + expect![[r#" + *foo* + + ```rust + ra_test_fixture + ``` + + ```rust + fn foo() + ``` + + --- + + Some docs, *not a bullet*. + "#]], + ); +} + #[test] fn hover_comments_dont_highlight_parent() { cov_mark::check!(no_highlight_on_comment_hover); @@ -11970,3 +11997,55 @@ fn main() { let _ = resolved.as_array(db); }); } + +#[test] +fn extern_c_fn_ptr_display() { + check( + r#" +extern "C" fn foo() {} + +fn bar() { + let v$0 = foo as extern "C" fn(); +} + "#, + expect![[r#" + *v* + + ```rust + let v: extern "C" fn() + ``` + + --- + + size = 8, align = 8, niches = 1, no Drop + "#]], + ); +} + +#[test] +fn subst_impl_trait_arg_with_const_generic() { + check( + r#" +fn main() { + generic$0_tn([()], 1); +} + +fn generic_tn(_: [T; N], _: impl Sized) {} +"#, + expect![[r#" + *generic_tn* + + ```rust + ra_test_fixture + ``` + + ```rust + fn generic_tn(_: [T; {const}], _: impl Sized) + ``` + + --- + + `T` = `()` + "#]], + ); +} diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html index c95b36a1b4f33..ede25fc2050e8 100644 --- a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html @@ -161,18 +161,18 @@ } /// ```rust -/// let _ = example(&[1, 2, 3]); +/// let _ = example(&[1, 2, 3]); /// ``` /// /// ``` -/// loop {} +/// loop {} #[cfg_attr(not(feature = "false"), doc = "loop {}")] #[doc = "loop {}"] /// ``` /// #[cfg_attr(feature = "alloc", doc = "```rust")] #[cfg_attr(not(feature = "alloc"), doc = "```ignore")] -/// let _ = example(&alloc::vec![1, 2, 3]); +/// let _ = example(&alloc::vec![1, 2, 3]); /// ``` pub fn mix_and_match() {} diff --git a/src/tools/rust-analyzer/crates/parser/src/grammar/patterns.rs b/src/tools/rust-analyzer/crates/parser/src/grammar/patterns.rs index f8be75b1787f0..fdc3f8c491eaf 100644 --- a/src/tools/rust-analyzer/crates/parser/src/grammar/patterns.rs +++ b/src/tools/rust-analyzer/crates/parser/src/grammar/patterns.rs @@ -66,6 +66,12 @@ fn pattern_r(p: &mut Parser<'_>, recovery_set: TokenSet) { fn pattern_single_r(p: &mut Parser<'_>, recovery_set: TokenSet) { // test range_pat // fn main() { + // match () { + // (..1) => (), + // (..=3) => (), + // (..2 | 4) => (), + // } + // // match 92 { // 0 ... 100 => (), // 101 ..= 200 => (), @@ -97,6 +103,7 @@ fn pattern_single_r(p: &mut Parser<'_>, recovery_set: TokenSet) { // (1.., _) => (), // (..=2, _) => (), // } + // // } if p.at(T![..=]) { @@ -484,8 +491,7 @@ fn tuple_pat(p: &mut Parser<'_>) -> CompletedMarker { p.error("expected a pattern"); break; } - has_rest |= p.at(T![..]); - + has_rest |= !p.at(T![..=]) && p.at(T![..]) && !RANGE_PAT_END_FIRST.contains(p.nth(2)); pattern(p); if !p.at(T![')']) { has_comma = true; diff --git a/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/range_pat.rast b/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/range_pat.rast index d9981c50719f3..ba0198e9d542c 100644 --- a/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/range_pat.rast +++ b/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/range_pat.rast @@ -12,6 +12,77 @@ SOURCE_FILE STMT_LIST L_CURLY "{" WHITESPACE "\n " + EXPR_STMT + MATCH_EXPR + MATCH_KW "match" + WHITESPACE " " + TUPLE_EXPR + L_PAREN "(" + R_PAREN ")" + WHITESPACE " " + MATCH_ARM_LIST + L_CURLY "{" + WHITESPACE "\n " + MATCH_ARM + PAREN_PAT + L_PAREN "(" + RANGE_PAT + DOT2 ".." + LITERAL_PAT + LITERAL + INT_NUMBER "1" + R_PAREN ")" + WHITESPACE " " + FAT_ARROW "=>" + WHITESPACE " " + TUPLE_EXPR + L_PAREN "(" + R_PAREN ")" + COMMA "," + WHITESPACE "\n " + MATCH_ARM + PAREN_PAT + L_PAREN "(" + RANGE_PAT + DOT2EQ "..=" + LITERAL_PAT + LITERAL + INT_NUMBER "3" + R_PAREN ")" + WHITESPACE " " + FAT_ARROW "=>" + WHITESPACE " " + TUPLE_EXPR + L_PAREN "(" + R_PAREN ")" + COMMA "," + WHITESPACE "\n " + MATCH_ARM + PAREN_PAT + L_PAREN "(" + OR_PAT + RANGE_PAT + DOT2 ".." + LITERAL_PAT + LITERAL + INT_NUMBER "2" + WHITESPACE " " + PIPE "|" + WHITESPACE " " + LITERAL_PAT + LITERAL + INT_NUMBER "4" + R_PAREN ")" + WHITESPACE " " + FAT_ARROW "=>" + WHITESPACE " " + TUPLE_EXPR + L_PAREN "(" + R_PAREN ")" + COMMA "," + WHITESPACE "\n " + R_CURLY "}" + WHITESPACE "\n\n " EXPR_STMT MATCH_EXPR MATCH_KW "match" @@ -468,6 +539,6 @@ SOURCE_FILE COMMA "," WHITESPACE "\n " R_CURLY "}" - WHITESPACE "\n" + WHITESPACE "\n\n" R_CURLY "}" WHITESPACE "\n" diff --git a/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/range_pat.rs b/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/range_pat.rs index b54354211d2dc..69e2da2cf046c 100644 --- a/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/range_pat.rs +++ b/src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/range_pat.rs @@ -1,4 +1,10 @@ fn main() { + match () { + (..1) => (), + (..=3) => (), + (..2 | 4) => (), + } + match 92 { 0 ... 100 => (), 101 ..= 200 => (), @@ -30,4 +36,5 @@ fn main() { (1.., _) => (), (..=2, _) => (), } + } diff --git a/src/tools/rust-analyzer/crates/proc-macro-srv/Cargo.toml b/src/tools/rust-analyzer/crates/proc-macro-srv/Cargo.toml index 1b86eac0129aa..05e0012586d5f 100644 --- a/src/tools/rust-analyzer/crates/proc-macro-srv/Cargo.toml +++ b/src/tools/rust-analyzer/crates/proc-macro-srv/Cargo.toml @@ -13,13 +13,11 @@ rust-version.workspace = true doctest = false [dependencies] -temp-dir.workspace = true - paths.workspace = true # span = {workspace = true, default-features = false} does not work span = { path = "../span", version = "0.0.0", default-features = false} intern.workspace = true - +stdx.workspace = true [dev-dependencies] expect-test.workspace = true diff --git a/src/tools/rust-analyzer/crates/proc-macro-srv/src/dylib.rs b/src/tools/rust-analyzer/crates/proc-macro-srv/src/dylib.rs index 3b9c345fc27f5..718eb47228eb8 100644 --- a/src/tools/rust-analyzer/crates/proc-macro-srv/src/dylib.rs +++ b/src/tools/rust-analyzer/crates/proc-macro-srv/src/dylib.rs @@ -2,13 +2,12 @@ mod proc_macros; +use paths::{Utf8Path, Utf8PathBuf}; use rustc_codegen_ssa::back::metadata::DefaultMetadataLoader; use rustc_interface::util::rustc_version_str; use rustc_proc_macro::bridge; -use std::{fs, io, time::SystemTime}; -use temp_dir::TempDir; - -use paths::{Utf8Path, Utf8PathBuf}; +use std::{fs, io, path::Path, time::SystemTime}; +use stdx::tempfile::NamedTempFile; use crate::{ PanicMessage, ProcMacroClientHandle, ProcMacroKind, ProcMacroSrvSpan, TrackedEnv, @@ -18,19 +17,20 @@ use crate::{ pub(crate) struct Expander { inner: ProcMacroLibrary, modified_time: SystemTime, + _file: NamedTempFile, } impl Expander { - pub(crate) fn new(temp_dir: &TempDir, lib: &Utf8Path) -> io::Result { + pub(crate) fn new(lib: &Utf8Path) -> io::Result { // Some libraries for dynamic loading require canonicalized path even when it is // already absolute let lib = lib.canonicalize_utf8()?; let modified_time = fs::metadata(&lib).and_then(|it| it.modified())?; - let path = ensure_file_with_lock_free_access(temp_dir, &lib)?; - let library = ProcMacroLibrary::open(path.as_ref())?; + let file = ensure_file_with_lock_free_access(lib); + let library = ProcMacroLibrary::open(file.path())?; - Ok(Expander { inner: library, modified_time }) + Ok(Expander { inner: library, modified_time, _file: file }) } pub(crate) fn expand<'a, S: ProcMacroSrvSpan + 'a>( @@ -73,10 +73,10 @@ struct ProcMacroLibrary { } impl ProcMacroLibrary { - fn open(path: &Utf8Path) -> io::Result { + fn open(path: &Path) -> io::Result { let proc_macros = rustc_span::create_default_session_globals_then(|| { rustc_metadata::locator::get_proc_macros( - path.as_ref(), + path, &DefaultMetadataLoader, rustc_version_str().unwrap_or("unknown"), ) @@ -88,37 +88,25 @@ impl ProcMacroLibrary { /// Copy the dylib to temp directory to prevent locking in Windows #[cfg(windows)] -fn ensure_file_with_lock_free_access( - temp_dir: &TempDir, - path: &Utf8Path, -) -> io::Result { - use std::collections::hash_map::RandomState; - use std::hash::{BuildHasher, Hasher}; - +fn ensure_file_with_lock_free_access(path: Utf8PathBuf) -> NamedTempFile { if std::env::var("RA_DONT_COPY_PROC_MACRO_DLL").is_ok() { - return Ok(path.to_path_buf()); + return NamedTempFile::from_path(path.into_std_path_buf()); } - let mut to = Utf8Path::from_path(temp_dir.path()).unwrap().to_owned(); - - let file_name = path.file_stem().ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidInput, format!("File path is invalid: {path}")) - })?; + (|| { + let file_name = path.file_stem().ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, format!("File path is invalid: {path}")) + })?; - to.push({ - // Generate a unique number by abusing `HashMap`'s hasher. - // Maybe this will also "inspire" a libs team member to finally put `rand` in libstd. - let unique_name = RandomState::new().build_hasher().finish(); - format!("{file_name}-{unique_name}.dll") - }); - fs::copy(path, &to)?; - Ok(to) + NamedTempFile::new_from_existing( + &format!("proc-macro-srv-{file_name}.dll"), + path.as_std_path(), + ) + })() + .unwrap_or_else(|_err| NamedTempFile::from_path(path.into_std_path_buf())) } #[cfg(unix)] -fn ensure_file_with_lock_free_access( - _temp_dir: &TempDir, - path: &Utf8Path, -) -> io::Result { - Ok(path.to_owned()) +fn ensure_file_with_lock_free_access(path: Utf8PathBuf) -> NamedTempFile { + NamedTempFile::from_path(path.into_std_path_buf()) } diff --git a/src/tools/rust-analyzer/crates/proc-macro-srv/src/lib.rs b/src/tools/rust-analyzer/crates/proc-macro-srv/src/lib.rs index 28570e1af4426..7fc04a05155f8 100644 --- a/src/tools/rust-analyzer/crates/proc-macro-srv/src/lib.rs +++ b/src/tools/rust-analyzer/crates/proc-macro-srv/src/lib.rs @@ -41,7 +41,6 @@ use std::{ use paths::{Utf8Path, Utf8PathBuf}; use span::{FIXUP_ERASED_FILE_AST_ID_MARKER, Span}; -use temp_dir::TempDir; pub use crate::server_impl::token_id::SpanId; @@ -64,16 +63,11 @@ pub const RUSTC_VERSION_STRING: &str = env!("RUSTC_VERSION"); pub struct ProcMacroSrv<'env> { expanders: Mutex>>, env: &'env EnvSnapshot, - temp_dir: TempDir, } impl<'env> ProcMacroSrv<'env> { pub fn new(env: &'env EnvSnapshot) -> Self { - Self { - expanders: Default::default(), - env, - temp_dir: TempDir::with_prefix("proc-macro-srv").unwrap(), - } + Self { expanders: Default::default(), env } } pub fn join_spans(&self, first: Span, second: Span) -> Option { @@ -205,7 +199,7 @@ impl ProcMacroSrv<'_> { fn expander(&self, path: &Utf8Path) -> Result, String> { let expander = || { - let expander = dylib::Expander::new(&self.temp_dir, path) + let expander = dylib::Expander::new(path) .map_err(|err| format!("Cannot create expander for {path}: {err}",)); expander.map(Arc::new) }; diff --git a/src/tools/rust-analyzer/crates/proc-macro-srv/src/tests/utils.rs b/src/tools/rust-analyzer/crates/proc-macro-srv/src/tests/utils.rs index 9780bcf3481b4..7f92c66fb69d5 100644 --- a/src/tools/rust-analyzer/crates/proc-macro-srv/src/tests/utils.rs +++ b/src/tools/rust-analyzer/crates/proc-macro-srv/src/tests/utils.rs @@ -56,7 +56,7 @@ fn assert_expand_impl( expect_spanned: Expect, ) { let path = proc_macro_test_dylib_path(); - let expander = dylib::Expander::new(&temp_dir::TempDir::new().unwrap(), &path).unwrap(); + let expander = dylib::Expander::new(&path).unwrap(); let def_site = SpanId(0); let call_site = SpanId(1); @@ -186,7 +186,7 @@ pub fn assert_expand_with_callback( expect_spanned: Expect, ) { let path = proc_macro_test_dylib_path(); - let expander = dylib::Expander::new(&temp_dir::TempDir::new().unwrap(), &path).unwrap(); + let expander = dylib::Expander::new(&path).unwrap(); let def_site = Span { range: TextRange::new(0.into(), 150.into()), diff --git a/src/tools/rust-analyzer/crates/project-model/Cargo.toml b/src/tools/rust-analyzer/crates/project-model/Cargo.toml index f825a456dea70..86ae3e837d1a8 100644 --- a/src/tools/rust-analyzer/crates/project-model/Cargo.toml +++ b/src/tools/rust-analyzer/crates/project-model/Cargo.toml @@ -20,7 +20,6 @@ semver.workspace = true serde_json.workspace = true serde.workspace = true serde_derive.workspace = true -temp-dir.workspace = true toml.workspace = true tracing = { workspace = true, features = ["attributes"] } triomphe.workspace = true diff --git a/src/tools/rust-analyzer/crates/project-model/src/cargo_config_file.rs b/src/tools/rust-analyzer/crates/project-model/src/cargo_config_file.rs index defd9f96ab5fb..126c0b41bb9f4 100644 --- a/src/tools/rust-analyzer/crates/project-model/src/cargo_config_file.rs +++ b/src/tools/rust-analyzer/crates/project-model/src/cargo_config_file.rs @@ -1,6 +1,7 @@ //! Read `.cargo/config.toml` as a TOML table use paths::{AbsPath, Utf8Path, Utf8PathBuf}; use rustc_hash::FxHashMap; +use stdx::tempfile::NamedTempDir; use toml::{ Spanned, de::{DeTable, DeValue}, @@ -139,7 +140,7 @@ impl<'a> CargoConfigFileReader<'a> { pub(crate) struct LockfileCopy { pub(crate) path: Utf8PathBuf, pub(crate) usage: LockfileUsage, - _temp_dir: temp_dir::TempDir, + _temp_dir: NamedTempDir, } pub(crate) enum LockfileUsage { @@ -193,22 +194,11 @@ pub(crate) fn make_lockfile_copy( return None; }; - let temp_dir = temp_dir::TempDir::with_prefix("rust-analyzer").ok()?; - let path: Utf8PathBuf = temp_dir.path().join("Cargo.lock").try_into().ok()?; - let path = match std::fs::copy(lockfile_path, &path) { - Ok(_) => { - tracing::debug!("Copied lock file from `{}` to `{}`", lockfile_path, path); - path - } - // lockfile does not yet exist, so we can just create a new one in the temp dir - Err(e) if e.kind() == std::io::ErrorKind::NotFound => path, - Err(e) => { - tracing::warn!("Failed to copy lock file from `{lockfile_path}` to `{path}`: {e}",); - return None; - } - }; + let temp_dir = NamedTempDir::new("rust-analyzer").ok()?; + let path = temp_dir.path().join("Cargo.lock"); + std::fs::copy(lockfile_path.as_std_path(), &path).ok()?; - Some(LockfileCopy { path, usage, _temp_dir: temp_dir }) + Some(LockfileCopy { path: Utf8PathBuf::from_path_buf(path).ok()?, usage, _temp_dir: temp_dir }) } #[test] diff --git a/src/tools/rust-analyzer/crates/stdx/src/lib.rs b/src/tools/rust-analyzer/crates/stdx/src/lib.rs index 275e0e5ac8db1..dcba06415b5f3 100644 --- a/src/tools/rust-analyzer/crates/stdx/src/lib.rs +++ b/src/tools/rust-analyzer/crates/stdx/src/lib.rs @@ -13,6 +13,7 @@ pub mod non_empty_vec; pub mod panic_context; pub mod process; pub mod rand; +pub mod tempfile; pub mod thread; pub mod variance; diff --git a/src/tools/rust-analyzer/crates/stdx/src/tempfile.rs b/src/tools/rust-analyzer/crates/stdx/src/tempfile.rs new file mode 100644 index 0000000000000..fe9ae83ef539e --- /dev/null +++ b/src/tools/rust-analyzer/crates/stdx/src/tempfile.rs @@ -0,0 +1,189 @@ +//! A temporary named file that will be deleted on drop, and on operating systems that support that, +//! also when the process exits (including being killed). + +use std::{ + fs::File, + io, + path::{Path, PathBuf}, +}; + +pub struct NamedTempFile { + _file: Option, + path: PathBuf, + delete_on_drop: bool, +} + +impl NamedTempFile { + pub fn new(prefix: &str) -> io::Result { + imp::create(prefix) + } + + /// Creates a new `NamedTempFile` that is a copy of an existing file. + pub fn new_from_existing(prefix: &str, existing: &Path) -> io::Result { + let result = NamedTempFile::new(prefix)?; + std::fs::copy(existing, &result.path)?; + Ok(result) + } + + /// Creates a `NamedTempFile` from a path, without deleting it on drop. + #[inline] + pub fn from_path(path: PathBuf) -> NamedTempFile { + NamedTempFile { _file: None, path, delete_on_drop: false } + } + + #[inline] + pub fn path(&self) -> &Path { + &self.path + } +} + +impl Drop for NamedTempFile { + fn drop(&mut self) { + if self.delete_on_drop && std::fs::remove_file(&self.path).is_err() { + tracing::info!("cannot remove temporary file {}", self.path.display()); + } + } +} + +pub struct NamedTempDir { + path: PathBuf, +} + +impl NamedTempDir { + pub fn new(prefix: &str) -> io::Result { + general_imp::create(prefix, |_options, path| std::fs::create_dir(path)) + .map(|((), path)| NamedTempDir { path }) + } + + #[inline] + pub fn path(&self) -> &Path { + &self.path + } +} + +impl Drop for NamedTempDir { + fn drop(&mut self) { + if std::fs::remove_dir_all(&self.path).is_err() { + tracing::info!("cannot remove temporary directory {}", self.path.display()); + } + } +} + +mod general_imp { + use std::{ + fs::OpenOptions, + io::{self, ErrorKind}, + path::{Path, PathBuf}, + sync::atomic::{AtomicU32, Ordering}, + }; + + static INTERNAL_COUNTER: AtomicU32 = AtomicU32::new(0); + + pub(super) fn create( + prefix: &str, + mut create: impl FnMut(OpenOptions, &Path) -> io::Result, + ) -> io::Result<(T, PathBuf)> { + let temp_dir = std::env::temp_dir().canonicalize()?; + let pid = std::process::id(); + loop { + let path = temp_dir.join(format!( + "{prefix}{pid:x}-{:x}", + INTERNAL_COUNTER.fetch_add(1, Ordering::AcqRel), + )); + let mut open_options = OpenOptions::new(); + open_options.create_new(true); + match create(open_options, &path) { + Err(e) if e.kind() == ErrorKind::AlreadyExists => {} + Err(e) => { + return Err(io::Error::new( + e.kind(), + format!("error creating directory {path:?}: {e}"), + )); + } + Ok(file) => { + return Ok((file, path)); + } + } + } + } +} + +#[cfg(any( + target_os = "linux", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", +))] +mod imp { + use std::{ + ffi::CString, + io, + os::{ + fd::{AsRawFd, RawFd}, + unix::ffi::OsStrExt, + }, + }; + + use super::*; + + #[cfg(target_os = "linux")] + fn path_after_unlink(fd: RawFd) -> PathBuf { + PathBuf::from(format!("/proc/self/fd/{fd}")) + } + + #[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))] + fn path_after_unlink(fd: RawFd) -> PathBuf { + PathBuf::from(format!("/dev/fd/{fd}")) + } + + pub(super) fn create(prefix: &str) -> io::Result { + let (file, mut path) = general_imp::create(prefix, |options, path| options.open(path))?; + let mut delete_on_drop = true; + if let Ok(original_path) = CString::new(path.as_os_str().as_bytes()) { + // Unlinking the file will *not* remove it per the POSIX specification since it is open. + // We cannot use `std::fs::remove_file()`, since, while currently using `unlink()`, it does + // not guarantee it will use it. + if unsafe { libc::unlink(original_path.as_ptr()) } == 0 { + path = path_after_unlink(file.as_raw_fd()); + delete_on_drop = false; + } + } + Ok(NamedTempFile { _file: Some(file), path, delete_on_drop }) + } +} + +#[cfg(windows)] +mod imp { + use std::os::windows::fs::OpenOptionsExt; + + use super::*; + + const FILE_ATTRIBUTE_TEMPORARY: u32 = 0x100; + const FILE_FLAG_DELETE_ON_CLOSE: u32 = 0x04000000; + + pub(super) fn create(prefix: &str) -> io::Result { + let (file, path) = general_imp::create(prefix, |mut options, path| { + options + .attributes(FILE_ATTRIBUTE_TEMPORARY) + .custom_flags(FILE_FLAG_DELETE_ON_CLOSE) + .open(path) + })?; + Ok(NamedTempFile { _file: Some(file), path, delete_on_drop: false }) + } +} + +#[cfg(not(any( + target_os = "linux", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + windows, +)))] +mod imp { + use super::*; + + pub(super) fn create(prefix: &str) -> io::Result { + let (file, path) = general_imp::create(prefix, |options, path| options.open(path))?; + Ok(NamedTempFile { _file: Some(file), path, delete_on_drop: true }) + } +} diff --git a/src/tools/rust-analyzer/crates/syntax/src/ast/make.rs b/src/tools/rust-analyzer/crates/syntax/src/ast/make.rs index 9017bae474273..36f7b56e3cd2a 100644 --- a/src/tools/rust-analyzer/crates/syntax/src/ast/make.rs +++ b/src/tools/rust-analyzer/crates/syntax/src/ast/make.rs @@ -1383,7 +1383,7 @@ fn expr_from_text_with_edition + AstNode>(text: &str, edition Some(it) => it, None => { let node = std::any::type_name::(); - panic!("Failed to make ast node `{node}` from text {text}") + panic!("Failed to make expr node `{node}` from text `{text}`") } }; let node = node.clone_subtree(); @@ -1403,7 +1403,7 @@ fn ast_from_text_with_edition(text: &str, edition: Edition) -> N { Some(it) => it, None => { let node = std::any::type_name::(); - panic!("Failed to make ast node `{node}` from text {text}") + panic!("Failed to make ast node `{node}` from text `{text}`") } }; let node = node.clone_subtree(); diff --git a/src/tools/rust-analyzer/crates/syntax/src/syntax_editor/edits.rs b/src/tools/rust-analyzer/crates/syntax/src/syntax_editor/edits.rs index 9fab8716b412f..35e9b8d2f87d0 100644 --- a/src/tools/rust-analyzer/crates/syntax/src/syntax_editor/edits.rs +++ b/src/tools/rust-analyzer/crates/syntax/src/syntax_editor/edits.rs @@ -528,7 +528,7 @@ mod tests { Some(it) => it, None => { let node = std::any::type_name::(); - panic!("Failed to make ast node `{node}` from text {text}") + panic!("Failed to make ast node `{node}` from text `{text}`") } }; let node = node.clone_subtree(); diff --git a/src/tools/rust-analyzer/crates/tt/src/storage.rs b/src/tools/rust-analyzer/crates/tt/src/storage.rs index 150777cc39e55..ba7a661b5e8a8 100644 --- a/src/tools/rust-analyzer/crates/tt/src/storage.rs +++ b/src/tools/rust-analyzer/crates/tt/src/storage.rs @@ -12,9 +12,6 @@ use std::{assert_matches, collections::hash_map, fmt::Debug, hint::cold_path, me #[cfg(all(debug_assertions, not(miri)))] use std::cell::Cell; -#[cfg(not(all(debug_assertions, not(miri))))] -use std::mem::MaybeUninit; - use intern::Symbol; use rustc_hash::FxHashMap; use span::{Span, SpanAnchor, SyntaxContext, TextRange, TextSize}; @@ -105,7 +102,7 @@ struct UninitBuffer { #[cfg(all(debug_assertions, not(miri)))] buffer: Box<[u8]>, #[cfg(not(all(debug_assertions, not(miri))))] - buffer: Box<[MaybeUninit]>, + buffer: Box<[std::mem::MaybeUninit]>, } impl UninitBuffer { @@ -807,10 +804,6 @@ unsafe fn decode_extended_span( } } -// FIXME: It'll probably be better to ensure this ourselves via a `#[repr(C, align(4))]` wrapper, even though practically -// this holds for all 32- and 64-bit targets (Rust does not guarantee this). -const _: () = assert!(align_of::<*const *const str>() >= 4); // Needed for the tagging of idents. - unsafe fn decode_symbol<'a>( mut ptr: BufferReader<'a>, first_byte: u8, @@ -830,8 +823,6 @@ unsafe fn decode_symbol<'a>( (ptr, symbols[symbol_idx as usize].clone()) } -/// We need `MaybeUninit` to preserve provenance. -/// /// The returned `u32` is the length of the children *in bytes*, if we read a subtree. Otherwise it's zero. unsafe fn decode<'a>( mut ptr: BufferReader<'a>,