Repository: rust-lang/rust-analyzer (rust)

Issue:
'Convert while to loop' assist removes comments from inside the while block
<!--
Troubleshooting guide: https://rust-analyzer.github.io/manual.html#troubleshooting
Forum for questions: https://users.rust-lang.org/c/ide/14

Before submitting, please make sure that you're not running into one of these known issues:

 1. on-the-fly diagnostics are mostly unimplemented (`cargo check` diagnostics will be shown when saving a file): #3107

Otherwise please try to provide information which will help us to fix the issue faster. Minimal reproducible examples with few dependencies are especially lovely <3.
-->

**rust-analyzer version**: 0.4.2068-standalone

**rustc version**: rustc 1.80.1 (3f5fd8dd4 2024-08-06)

**editor or extension**: code oss

**relevant settings**: None

**code snippet to reproduce**: 
```rust
let mut i = 0;

while i < 5 {
    // comment 1
    dbg!(i);
    // comment 2
    i += 1;
    // comment 3
}
```

Converting this `while` loop to a `loop` loop using the assist removes all of the comments:
```rust
let mut i = 0;

loop {
    if i >= 5 {
        break;
    }
    dbg!(i);
    i += 1;
}
```

Pull request description:
fix: keep comments in convert_while_to_loop
Fix #17869.

Patch:
diff --git a/crates/hir-ty/src/lower.rs b/crates/hir-ty/src/lower.rs
index 5824c59c5a..79b096068d 100644
--- a/crates/hir-ty/src/lower.rs
+++ b/crates/hir-ty/src/lower.rs
@@ -1976,13 +1976,13 @@ fn type_for_type_alias(db: &dyn HirDatabase, t: TypeAliasId) -> Binders<Ty> {
         .with_impl_trait_mode(ImplTraitLoweringMode::Opaque)
         .with_type_param_mode(ParamLoweringMode::Variable);
     let type_alias_data = db.type_alias_data(t);
-    let inner = if type_alias_data.is_extern {
-        TyKind::Foreign(crate::to_foreign_def_id(t)).intern(Interner)
+    if type_alias_data.is_extern {
+        Binders::empty(Interner, TyKind::Foreign(crate::to_foreign_def_id(t)).intern(Interner))
     } else {
         let type_ref = &type_alias_data.type_ref;
-        ctx.lower_ty(type_ref.as_deref().unwrap_or(&TypeRef::Error))
-    };
-    make_binders(db, &generics, inner)
+        let inner = ctx.lower_ty(type_ref.as_deref().unwrap_or(&TypeRef::Error));
+        make_binders(db, &generics, inner)
+    }
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
diff --git a/crates/ide-assists/src/handlers/convert_while_to_loop.rs b/crates/ide-assists/src/handlers/convert_while_to_loop.rs
index c34b684112..434daa279c 100644
--- a/crates/ide-assists/src/handlers/convert_while_to_loop.rs
+++ b/crates/ide-assists/src/handlers/convert_while_to_loop.rs
@@ -1,5 +1,6 @@
-use std::iter::once;
+use std::iter;
 
+use either::Either;
 use ide_db::syntax_helpers::node_ext::is_pattern_cond;
 use syntax::{
     ast::{
@@ -52,18 +53,30 @@ pub(crate) fn convert_while_to_loop(acc: &mut Assists, ctx: &AssistContext<'_>)
         |edit| {
             let while_indent_level = IndentLevel::from_node(while_expr.syntax());
 
-            let break_block =
-                make::block_expr(once(make::expr_stmt(make::expr_break(None, None)).into()), None)
-                    .indent(while_indent_level);
+            let break_block = make::block_expr(
+                iter::once(make::expr_stmt(make::expr_break(None, None)).into()),
+                None,
+            )
+            .indent(while_indent_level);
             let block_expr = if is_pattern_cond(while_cond.clone()) {
                 let if_expr = make::expr_if(while_cond, while_body, Some(break_block.into()));
-                let stmts = once(make::expr_stmt(if_expr).into());
+                let stmts = iter::once(make::expr_stmt(if_expr).into());
                 make::block_expr(stmts, None)
             } else {
                 let if_cond = invert_boolean_expression(while_cond);
-                let if_expr = make::expr_if(if_cond, break_block, None);
-                let stmts = once(make::expr_stmt(if_expr).into()).chain(while_body.statements());
-                make::block_expr(stmts, while_body.tail_expr())
+                let if_expr = make::expr_if(if_cond, break_block, None).syntax().clone().into();
+                let elements = while_body.stmt_list().map_or_else(
+                    || Either::Left(iter::empty()),
+                    |stmts| {
+                        Either::Right(stmts.syntax().children_with_tokens().filter(|node_or_tok| {
+                            // Filter out the trailing expr
+                            !node_or_tok
+                                .as_node()
+                                .is_some_and(|node| ast::Expr::can_cast(node.kind()))
+                        }))
+                    },
+                );
+                make::hacky_block_expr(iter::once(if_expr).chain(elements), while_body.tail_expr())
             };
 
             let replacement = make::expr_loop(block_expr.indent(while_indent_level));
@@ -182,6 +195,74 @@ fn main() {
         bar();
     }
 }
+"#,
+        );
+    }
+
+    #[test]
+    fn preserve_comments() {
+        check_assist(
+            convert_while_to_loop,
+            r#"
+fn main() {
+    let mut i = 0;
+
+    $0while i < 5 {
+        // comment 1
+        dbg!(i);
+        // comment 2
+        i += 1;
+        // comment 3
+    }
+}
+"#,
+            r#"
+fn main() {
+    let mut i = 0;
+
+    loop {
+        if i >= 5 {
+            break;
+        }
+        // comment 1
+        dbg!(i);
+        // comment 2
+        i += 1;
+        // comment 3
+    }
+}
+"#,
+        );
+
+        check_assist(
+            convert_while_to_loop,
+            r#"
+fn main() {
+    let v = vec![1, 2, 3];
+    let iter = v.iter();
+
+    $0while let Some(i) = iter.next() {
+        // comment 1
+        dbg!(i);
+        // comment 2
+    }
+}
+"#,
+            r#"
+fn main() {
+    let v = vec![1, 2, 3];
+    let iter = v.iter();
+
+    loop {
+        if let Some(i) = iter.next() {
+            // comment 1
+            dbg!(i);
+            // comment 2
+        } else {
+            break;
+        }
+    }
+}
 "#,
         );
     }
diff --git a/crates/project-model/src/build_dependencies.rs b/crates/project-model/src/build_dependencies.rs
index 25d1ea07aa..e7a4b8f39f 100644
--- a/crates/project-model/src/build_dependencies.rs
+++ b/crates/project-model/src/build_dependencies.rs
@@ -19,8 +19,8 @@ use serde::Deserialize;
 use toolchain::Tool;
 
 use crate::{
-    utf8_stdout, CargoConfig, CargoFeatures, CargoWorkspace, InvocationStrategy, ManifestPath,
-    Package, Sysroot, TargetKind,
+    utf8_stdout, CargoConfig, CargoFeatures, CargoWorkspace, InvocationLocation,
+    InvocationStrategy, ManifestPath, Package, Sysroot, TargetKind,
 };
 
 /// Output of the build script and proc-macro building steps for a workspace.
@@ -63,7 +63,10 @@ impl WorkspaceBuildScripts {
         progress: &dyn Fn(String),
         sysroot: &Sysroot,
     ) -> io::Result<WorkspaceBuildScripts> {
-        let current_dir = workspace.workspace_root();
+        let current_dir = match &config.invocation_location {
+            InvocationLocation::Root(root) if config.run_build_script_command.is_some() => root,
+            _ => workspace.workspace_root(),
+        };
 
         let allowed_features = workspace.workspace_features();
         let cmd = Self::build_command(
@@ -86,7 +89,15 @@ impl WorkspaceBuildScripts {
     ) -> io::Result<Vec<WorkspaceBuildScripts>> {
         assert_eq!(config.invocation_strategy, InvocationStrategy::Once);
 
-        let current_dir = workspace_root;
+        let current_dir = match &config.invocation_location {
+            InvocationLocation::Root(root) => root,
+            InvocationLocation::Workspace => {
+                return Err(io::Error::new(
+                    io::ErrorKind::Other,
+                    "Cannot run build scripts from workspace with invocation strategy `once`",
+                ))
+            }
+        };
         let cmd = Self::build_command(
             config,
             &Default::default(),
diff --git a/crates/project-model/src/cargo_workspace.rs b/crates/project-model/src/cargo_workspace.rs
index 492bc9a925..db9c20fdc5 100644
--- a/crates/project-model/src/cargo_workspace.rs
+++ b/crates/project-model/src/cargo_workspace.rs
@@ -13,7 +13,7 @@ use serde_json::from_value;
 use span::Edition;
 use toolchain::Tool;
 
-use crate::{utf8_stdout, ManifestPath, Sysroot};
+use crate::{utf8_stdout, InvocationLocation, ManifestPath, Sysroot};
 use crate::{CfgOverrides, InvocationStrategy};
 
 /// [`CargoWorkspace`] represents the logical structure of, well, a Cargo
@@ -100,6 +100,7 @@ pub struct CargoConfig {
     /// Extra env vars to set when invoking the cargo command
     pub extra_env: FxHashMap<String, String>,
     pub invocation_strategy: InvocationStrategy,
+    pub invocation_location: InvocationLocation,
     /// Optional path to use instead of `target` when building
     pub target_dir: Option<Utf8PathBuf>,
 }
diff --git a/crates/project-model/src/lib.rs b/crates/project-model/src/lib.rs
index b8ac55ed0d..4fa70508bb 100644
--- a/crates/project-model/src/lib.rs
+++ b/crates/project-model/src/lib.rs
@@ -186,13 +186,20 @@ fn utf8_stdout(mut cmd: Command) -> anyhow::Result<String> {
     Ok(stdout.trim().to_owned())
 }
 
-#[derive(Clone, Debug, Default, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
 pub enum InvocationStrategy {
     Once,
     #[default]
     PerWorkspace,
 }
 
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub enum InvocationLocation {
+    Root(AbsPathBuf),
+    #[default]
+    Workspace,
+}
+
 /// A set of cfg-overrides per crate.
 #[derive(Default, Debug, Clone, Eq, PartialEq)]
 pub struct CfgOverrides {
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index 680a500eaa..02f5d75136 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -80,6 +80,13 @@ config_data! {
         pub(crate) cargo_autoreload: bool           = true,
         /// Run build scripts (`build.rs`) for more precise code analysis.
         cargo_buildScripts_enable: bool  = true,
+        /// Specifies the working directory for running build scripts.
+        /// - "workspace": run build scripts for a workspace in the workspace's root directory.
+        ///   This is incompatible with `#rust-analyzer.cargo.buildScripts.invocationStrategy#` set to `once`.
+        /// - "root": run build scripts in the project's root directory.
+        /// This config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`
+        /// is set.
+        cargo_buildScripts_invocationLocation: InvocationLocation = InvocationLocation::Workspace,
         /// Specifies the invocation strategy to use when running the build scripts command.
         /// If `per_workspace` is set, the command will be executed for each workspace.
         /// If `once` is set, the command will be executed once.
@@ -94,7 +101,8 @@ config_data! {
         /// If there are multiple linked projects/workspaces, this command is invoked for
         /// each of them, with the working directory being the workspace root
         /// (i.e., the folder containing the `Cargo.toml`). This can be overwritten
-        /// by changing `#rust-analyzer.cargo.buildScripts.invocationStrategy#`.
+        /// by changing `#rust-analyzer.cargo.buildScripts.invocationStrategy#` and
+        /// `#rust-analyzer.cargo.buildScripts.invocationLocation#`.
         ///
         /// By default, a cargo invocation will be constructed for the configured
         /// targets and features, with the following base command line:
@@ -174,6 +182,14 @@ config_data! {
         ///
         /// For example for `cargo check`: `dead_code`, `unused_imports`, `unused_variables`,...
         check_ignore: FxHashSet<String> = FxHashSet::default(),
+        /// Specifies the working directory for running checks.
+        /// - "workspace": run checks for workspaces in the corresponding workspaces' root directories.
+        // FIXME: Ideally we would support this in some way
+        ///   This falls back to "root" if `#rust-analyzer.check.invocationStrategy#` is set to `once`.
+        /// - "root": run checks in the project's root directory.
+        /// This config only has an effect when `#rust-analyzer.check.overrideCommand#`
+        /// is set.
+        check_invocationLocation | checkOnSave_invocationLocation: InvocationLocation = InvocationLocation::Workspace,
         /// Specifies the invocation strategy to use when running the check command.
         /// If `per_workspace` is set, the command will be executed for each workspace.
         /// If `once` is set, the command will be executed once.
@@ -196,7 +212,8 @@ config_data! {
         /// If there are multiple linked projects/workspaces, this command is invoked for
         /// each of them, with the working directory being the workspace root
         /// (i.e., the folder containing the `Cargo.toml`). This can be overwritten
-        /// by changing `#rust-analyzer.check.invocationStrategy#`.
+        /// by changing `#rust-analyzer.check.invocationStrategy#` and
+        /// `#rust-analyzer.check.invocationLocation#`.
         ///
         /// If `$saved_file` is part of the command, rust-analyzer will pass
         /// the absolute path of the saved file to the provided command. This is
@@ -1851,6 +1868,12 @@ impl Config {
                 InvocationStrategy::Once => project_model::InvocationStrategy::Once,
                 InvocationStrategy::PerWorkspace => project_model::InvocationStrategy::PerWorkspace,
             },
+            invocation_location: match self.cargo_buildScripts_invocationLocation(None) {
+                InvocationLocation::Root => {
+                    project_model::InvocationLocation::Root(self.root_path.clone())
+                }
+                InvocationLocation::Workspace => project_model::InvocationLocation::Workspace,
+            },
             run_build_script_command: self.cargo_buildScripts_overrideCommand(None).clone(),
             extra_args: self.cargo_extraArgs(None).clone(),
             extra_env: self.cargo_extraEnv(None).clone(),
@@ -1907,6 +1930,14 @@ impl Config {
                             crate::flycheck::InvocationStrategy::PerWorkspace
                         }
                     },
+                    invocation_location: match self.check_invocationLocation(None) {
+                        InvocationLocation::Root => {
+                            crate::flycheck::InvocationLocation::Root(self.root_path.clone())
+                        }
+                        InvocationLocation::Workspace => {
+                            crate::flycheck::InvocationLocation::Workspace
+                        }
+                    },
                 }
             }
             Some(_) | None => FlycheckConfig::CargoCommand {
@@ -2317,6 +2348,13 @@ pub(crate) enum InvocationStrategy {
 #[derive(Serialize, Deserialize, Debug, Clone)]
 struct CheckOnSaveTargets(#[serde(with = "single_or_array")] Vec<String>);
 
+#[derive(Serialize, Deserialize, Debug, Clone)]
+#[serde(rename_all = "snake_case")]
+enum InvocationLocation {
+    Root,
+    Workspace,
+}
+
 #[derive(Serialize, Deserialize, Debug, Clone)]
 #[serde(rename_all = "snake_case")]
 enum LifetimeElisionDef {
@@ -3158,6 +3196,14 @@ fn field_props(field: &str, ty: &str, doc: &[&str], default: &str) -> serde_json
                 "The command will be executed once."
             ],
         },
+        "InvocationLocation" => set! {
+            "type": "string",
+            "enum": ["workspace", "root"],
+            "enumDescriptions": [
+                "The command will be executed in the corresponding workspace root.",
+                "The command will be executed in the project root."
+            ],
+        },
         "Option<CheckOnSaveTargets>" => set! {
             "anyOf": [
                 {
diff --git a/crates/rust-analyzer/src/flycheck.rs b/crates/rust-analyzer/src/flycheck.rs
index cf13a73863..443f52c6dd 100644
--- a/crates/rust-analyzer/src/flycheck.rs
+++ b/crates/rust-analyzer/src/flycheck.rs
@@ -15,13 +15,20 @@ use toolchain::Tool;
 
 use crate::command::{CommandHandle, ParseFromLine};
 
-#[derive(Clone, Debug, Default, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
 pub(crate) enum InvocationStrategy {
     Once,
     #[default]
     PerWorkspace,
 }
 
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub(crate) enum InvocationLocation {
+    Root(AbsPathBuf),
+    #[default]
+    Workspace,
+}
+
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub(crate) struct CargoOptions {
     pub(crate) target_triples: Vec<String>,
@@ -72,6 +79,7 @@ pub(crate) enum FlycheckConfig {
         args: Vec<String>,
         extra_env: FxHashMap<String, String>,
         invocation_strategy: InvocationStrategy,
+        invocation_location: InvocationLocation,
     },
 }
 
@@ -416,17 +424,30 @@ impl FlycheckActor {
                 cmd.args(&options.extra_args);
                 Some(cmd)
             }
-            FlycheckConfig::CustomCommand { command, args, extra_env, invocation_strategy } => {
+            FlycheckConfig::CustomCommand {
+                command,
+                args,
+                extra_env,
+                invocation_strategy,
+                invocation_location,
+            } => {
                 let mut cmd = Command::new(command);
                 cmd.envs(extra_env);
 
-                match invocation_strategy {
-                    InvocationStrategy::Once => {
-                        cmd.current_dir(&self.root);
+                match invocation_location {
+                    InvocationLocation::Workspace => {
+                        match invocation_strategy {
+                            InvocationStrategy::Once => {
+                                cmd.current_dir(&self.root);
+                            }
+                            InvocationStrategy::PerWorkspace => {
+                                // FIXME: cmd.current_dir(&affected_workspace);
+                                cmd.current_dir(&self.root);
+                            }
+                        }
                     }
-                    InvocationStrategy::PerWorkspace => {
-                        // FIXME: cmd.current_dir(&affected_workspace);
-                        cmd.current_dir(&self.root);
+                    InvocationLocation::Root(root) => {
+                        cmd.current_dir(root);
                     }
                 }
 
diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs
index 510dc22603..71ed287268 100644
--- a/crates/rust-analyzer/src/reload.rs
+++ b/crates/rust-analyzer/src/reload.rs
@@ -764,22 +764,18 @@ impl GlobalState {
             FlycheckConfig::CargoCommand { .. } => {
                 crate::flycheck::InvocationStrategy::PerWorkspace
             }
-            FlycheckConfig::CustomCommand { ref invocation_strategy, .. } => {
-                invocation_strategy.clone()
-            }
+            FlycheckConfig::CustomCommand { invocation_strategy, .. } => invocation_strategy,
         };
 
         self.flycheck = match invocation_strategy {
-            crate::flycheck::InvocationStrategy::Once => {
-                vec![FlycheckHandle::spawn(
-                    0,
-                    sender,
-                    config,
-                    None,
-                    self.config.root_path().clone(),
-                    None,
-                )]
-            }
+            crate::flycheck::InvocationStrategy::Once => vec![FlycheckHandle::spawn(
+                0,
+                sender,
+                config,
+                None,
+                self.config.root_path().clone(),
+                None,
+            )],
             crate::flycheck::InvocationStrategy::PerWorkspace => {
                 self.workspaces
                     .iter()
diff --git a/docs/user/generated_config.adoc b/docs/user/generated_config.adoc
index 647cf3db4a..2be338dd4d 100644
--- a/docs/user/generated_config.adoc
+++ b/docs/user/generated_config.adoc
@@ -45,6 +45,16 @@ Automatically refresh project info via `cargo metadata` on
 --
 Run build scripts (`build.rs`) for more precise code analysis.
 --
+[[rust-analyzer.cargo.buildScripts.invocationLocation]]rust-analyzer.cargo.buildScripts.invocationLocation (default: `"workspace"`)::
++
+--
+Specifies the working directory for running build scripts.
+- "workspace": run build scripts for a workspace in the workspace's root directory.
+  This is incompatible with `#rust-analyzer.cargo.buildScripts.invocationStrategy#` set to `once`.
+- "root": run build scripts in the project's root directory.
+This config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`
+is set.
+--
 [[rust-analyzer.cargo.buildScripts.invocationStrategy]]rust-analyzer.cargo.buildScripts.invocationStrategy (default: `"per_workspace"`)::
 +
 --
@@ -65,7 +75,8 @@ option.
 If there are multiple linked projects/workspaces, this command is invoked for
 each of them, with the working directory being the workspace root
 (i.e., the folder containing the `Cargo.toml`). This can be overwritten
-by changing `#rust-analyzer.cargo.buildScripts.invocationStrategy#`.
+by changing `#rust-analyzer.cargo.buildScripts.invocationStrategy#` and
+`#rust-analyzer.cargo.buildScripts.invocationLocation#`.
 
 By default, a cargo invocation will be constructed for the configured
 targets and features, with the following base command line:
@@ -198,6 +209,16 @@ List of `cargo check` (or other command specified in `check.command`) diagnostic
 
 For example for `cargo check`: `dead_code`, `unused_imports`, `unused_variables`,...
 --
+[[rust-analyzer.check.invocationLocation]]rust-analyzer.check.invocationLocation (default: `"workspace"`)::
++
+--
+Specifies the working directory for running checks.
+- "workspace": run checks for workspaces in the corresponding workspaces' root directories.
+  This falls back to "root" if `#rust-analyzer.check.invocationStrategy#` is set to `once`.
+- "root": run checks in the project's root directory.
+This config only has an effect when `#rust-analyzer.check.overrideCommand#`
+is set.
+--
 [[rust-analyzer.check.invocationStrategy]]rust-analyzer.check.invocationStrategy (default: `"per_workspace"`)::
 +
 --
@@ -229,7 +250,8 @@ Cargo, you might also want to change
 If there are multiple linked projects/workspaces, this command is invoked for
 each of them, with the working directory being the workspace root
 (i.e., the folder containing the `Cargo.toml`). This can be overwritten
-by changing `#rust-analyzer.check.invocationStrategy#`.
+by changing `#rust-analyzer.check.invocationStrategy#` and
+`#rust-analyzer.check.invocationLocation#`.
 
 If `$saved_file` is part of the command, rust-analyzer will pass
 the absolute path of the saved file to the provided command. This is
diff --git a/editors/code/package.json b/editors/code/package.json
index 9c2c227319..3d1e1efe4d 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -663,6 +663,24 @@
                     }
                 }
             },
+            {
+                "title": "cargo",
+                "properties": {
+                    "rust-analyzer.cargo.buildScripts.invocationLocation": {
+                        "markdownDescription": "Specifies the working directory for running build scripts.\n- \"workspace\": run build scripts for a workspace in the workspace's root directory.\n    This is incompatible with `#rust-analyzer.cargo.buildScripts.invocationStrategy#` set to `once`.\n- \"root\": run build scripts in the project's root directory.\nThis config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`\nis set.",
+                        "default": "workspace",
+                        "type": "string",
+                        "enum": [
+                            "workspace",
+                            "root"
+                        ],
+                        "enumDescriptions": [
+                            "The command will be executed in the corresponding workspace root.",
+                            "The command will be executed in the project root."
+                        ]
+                    }
+                }
+            },
             {
                 "title": "cargo",
                 "properties": {
@@ -685,7 +703,7 @@
                 "title": "cargo",
                 "properties": {
                     "rust-analyzer.cargo.buildScripts.overrideCommand": {
-                        "markdownDescription": "Override the command rust-analyzer uses to run build scripts and\nbuild procedural macros. The command is required to output json\nand should therefore include `--message-format=json` or a similar\noption.\n\nIf there are multiple linked projects/workspaces, this command is invoked for\neach of them, with the working directory being the workspace root\n(i.e., the folder containing the `Cargo.toml`). This can be overwritten\nby changing `#rust-analyzer.cargo.buildScripts.invocationStrategy#`.\n\nBy default, a cargo invocation will be constructed for the configured\ntargets and features, with the following base command line:\n\n```bash\ncargo check --quiet --workspace --message-format=json --all-targets --keep-going\n```\n.",
+                        "markdownDescription": "Override the command rust-analyzer uses to run build scripts and\nbuild procedural macros. The command is required to output json\nand should therefore include `--message-format=json` or a similar\noption.\n\nIf there are multiple linked projects/workspaces, this command is invoked for\neach of them, with the working directory being the workspace root\n(i.e., the folder containing the `Cargo.toml`). This can be overwritten\nby changing `#rust-analyzer.cargo.buildScripts.invocationStrategy#` and\n`#rust-analyzer.cargo.buildScripts.invocationLocation#`.\n\nBy default, a cargo invocation will be constructed for the configured\ntargets and features, with the following base command line:\n\n```bash\ncargo check --quiet --workspace --message-format=json --all-targets --keep-going\n```\n.",
                         "default": null,
                         "type": [
                             "null",
@@ -947,6 +965,24 @@
                     }
                 }
             },
+            {
+                "title": "check",
+                "properties": {
+                    "rust-analyzer.check.invocationLocation": {
+                        "markdownDescription": "Specifies the working directory for running checks.\n- \"workspace\": run checks for workspaces in the corresponding workspaces' root directories.\n    This falls back to \"root\" if `#rust-analyzer.check.invocationStrategy#` is set to `once`.\n- \"root\": run checks in the project's root directory.\nThis config only has an effect when `#rust-analyzer.check.overrideCommand#`\nis set.",
+                        "default": "workspace",
+                        "type": "string",
+                        "enum": [
+                            "workspace",
+                            "root"
+                        ],
+                        "enumDescriptions": [
+                            "The command will be executed in the corresponding workspace root.",
+                            "The command will be executed in the project root."
+                        ]
+                    }
+                }
+            },
             {
                 "title": "check",
                 "properties": {
@@ -982,7 +1018,7 @@
                 "title": "check",
                 "properties": {
                     "rust-analyzer.check.overrideCommand": {
-                        "markdownDescription": "Override the command rust-analyzer uses instead of `cargo check` for\ndiagnostics on save. The command is required to output json and\nshould therefore include `--message-format=json` or a similar option\n(if your client supports the `colorDiagnosticOutput` experimental\ncapability, you can use `--message-format=json-diagnostic-rendered-ansi`).\n\nIf you're changing this because you're using some tool wrapping\nCargo, you might also want to change\n`#rust-analyzer.cargo.buildScripts.overrideCommand#`.\n\nIf there are multiple linked projects/workspaces, this command is invoked for\neach of them, with the working directory being the workspace root\n(i.e., the folder containing the `Cargo.toml`). This can be overwritten\nby changing `#rust-analyzer.check.invocationStrategy#`.\n\nIf `$saved_file` is part of the command, rust-analyzer will pass\nthe absolute path of the saved file to the provided command. This is\nintended to be used with non-Cargo build systems.\nNote that `$saved_file` is experimental and may be removed in the future.\n\nAn example command would be:\n\n```bash\ncargo check --workspace --message-format=json --all-targets\n```\n.",
+                        "markdownDescription": "Override the command rust-analyzer uses instead of `cargo check` for\ndiagnostics on save. The command is required to output json and\nshould therefore include `--message-format=json` or a similar option\n(if your client supports the `colorDiagnosticOutput` experimental\ncapability, you can use `--message-format=json-diagnostic-rendered-ansi`).\n\nIf you're changing this because you're using some tool wrapping\nCargo, you might also want to change\n`#rust-analyzer.cargo.buildScripts.overrideCommand#`.\n\nIf there are multiple linked projects/workspaces, this command is invoked for\neach of them, with the working directory being the workspace root\n(i.e., the folder containing the `Cargo.toml`). This can be overwritten\nby changing `#rust-analyzer.check.invocationStrategy#` and\n`#rust-analyzer.check.invocationLocation#`.\n\nIf `$saved_file` is part of the command, rust-analyzer will pass\nthe absolute path of the saved file to the provided command. This is\nintended to be used with non-Cargo build systems.\nNote that `$saved_file` is experimental and may be removed in the future.\n\nAn example command would be:\n\n```bash\ncargo check --workspace --message-format=json --all-targets\n```\n.",
                         "default": null,
                         "type": [
                             "null",


Test patch:
diff --git a/crates/hir-ty/src/tests/regression.rs b/crates/hir-ty/src/tests/regression.rs
index d6e034b018..01862aa2a3 100644
--- a/crates/hir-ty/src/tests/regression.rs
+++ b/crates/hir-ty/src/tests/regression.rs
@@ -2195,19 +2195,3 @@ impl<'a, T: Deref<Target = impl Trait>> Struct<'a, T> {
         "#]],
     );
 }
-
-#[test]
-fn issue_17767() {
-    check_infer(
-        r#"
-extern "C" {
-    type Foo<T>;
-}
-
-fn f() -> Foo {}
-"#,
-        expect![[r#"
-            47..49 '{}': Foo
-        "#]],
-    );
-}