about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/tools/rust-analyzer/.gitignore3
-rw-r--r--src/tools/rust-analyzer/crates/hir/src/semantics.rs8
-rw-r--r--src/tools/rust-analyzer/crates/hir/src/source_analyzer.rs19
-rw-r--r--src/tools/rust-analyzer/crates/ide-db/src/defs.rs26
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/goto_definition.rs93
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/hover/tests.rs153
-rw-r--r--src/tools/rust-analyzer/crates/rust-analyzer/src/bin/main.rs4
-rw-r--r--src/tools/rust-analyzer/crates/rust-analyzer/src/cli/lsif.rs35
-rw-r--r--src/tools/rust-analyzer/crates/rust-analyzer/tests/slow-tests/cli.rs131
-rw-r--r--src/tools/rust-analyzer/crates/rust-analyzer/tests/slow-tests/main.rs1
-rw-r--r--src/tools/rust-analyzer/crates/rust-analyzer/tests/slow-tests/support.rs42
11 files changed, 489 insertions, 26 deletions
diff --git a/src/tools/rust-analyzer/.gitignore b/src/tools/rust-analyzer/.gitignore
index 68c87a6b1ed..c4470a45078 100644
--- a/src/tools/rust-analyzer/.gitignore
+++ b/src/tools/rust-analyzer/.gitignore
@@ -1,6 +1,5 @@
-/target/
+target/
 /dist/
-crates/*/target
 **/*.rs.bk
 **/*.rs.pending-snap
 .idea/*
diff --git a/src/tools/rust-analyzer/crates/hir/src/semantics.rs b/src/tools/rust-analyzer/crates/hir/src/semantics.rs
index 2716a10343b..d0e45f530bc 100644
--- a/src/tools/rust-analyzer/crates/hir/src/semantics.rs
+++ b/src/tools/rust-analyzer/crates/hir/src/semantics.rs
@@ -203,6 +203,10 @@ impl<DB: HirDatabase> Semantics<'_, DB> {
         self.imp.descend_node_at_offset(node, offset).filter_map(|mut it| it.find_map(N::cast))
     }
 
+    pub fn resolve_range_pat(&self, range_pat: &ast::RangePat) -> Option<Struct> {
+        self.imp.resolve_range_pat(range_pat).map(Struct::from)
+    }
+
     pub fn resolve_range_expr(&self, range_expr: &ast::RangeExpr) -> Option<Struct> {
         self.imp.resolve_range_expr(range_expr).map(Struct::from)
     }
@@ -1367,6 +1371,10 @@ impl<'db> SemanticsImpl<'db> {
         self.analyze(call.syntax())?.resolve_method_call_fallback(self.db, call)
     }
 
+    fn resolve_range_pat(&self, range_pat: &ast::RangePat) -> Option<StructId> {
+        self.analyze(range_pat.syntax())?.resolve_range_pat(self.db, range_pat)
+    }
+
     fn resolve_range_expr(&self, range_expr: &ast::RangeExpr) -> Option<StructId> {
         self.analyze(range_expr.syntax())?.resolve_range_expr(self.db, range_expr)
     }
diff --git a/src/tools/rust-analyzer/crates/hir/src/source_analyzer.rs b/src/tools/rust-analyzer/crates/hir/src/source_analyzer.rs
index be0e57b0b1d..9d32229e8f7 100644
--- a/src/tools/rust-analyzer/crates/hir/src/source_analyzer.rs
+++ b/src/tools/rust-analyzer/crates/hir/src/source_analyzer.rs
@@ -348,6 +348,25 @@ impl SourceAnalyzer {
         }
     }
 
+    pub(crate) fn resolve_range_pat(
+        &self,
+        db: &dyn HirDatabase,
+        range_pat: &ast::RangePat,
+    ) -> Option<StructId> {
+        let path: ModPath = match (range_pat.op_kind()?, range_pat.start(), range_pat.end()) {
+            (RangeOp::Exclusive, None, Some(_)) => path![core::ops::RangeTo],
+            (RangeOp::Exclusive, Some(_), None) => path![core::ops::RangeFrom],
+            (RangeOp::Exclusive, Some(_), Some(_)) => path![core::ops::Range],
+            (RangeOp::Inclusive, None, Some(_)) => path![core::ops::RangeToInclusive],
+            (RangeOp::Inclusive, Some(_), Some(_)) => path![core::ops::RangeInclusive],
+
+            (RangeOp::Exclusive, None, None) => return None,
+            (RangeOp::Inclusive, None, None) => return None,
+            (RangeOp::Inclusive, Some(_), None) => return None,
+        };
+        self.resolver.resolve_known_struct(db.upcast(), &path)
+    }
+
     pub(crate) fn resolve_range_expr(
         &self,
         db: &dyn HirDatabase,
diff --git a/src/tools/rust-analyzer/crates/ide-db/src/defs.rs b/src/tools/rust-analyzer/crates/ide-db/src/defs.rs
index a253e086f81..fdac4dd2efb 100644
--- a/src/tools/rust-analyzer/crates/ide-db/src/defs.rs
+++ b/src/tools/rust-analyzer/crates/ide-db/src/defs.rs
@@ -178,7 +178,19 @@ impl Definition {
             Definition::Static(it) => it.docs(db),
             Definition::Trait(it) => it.docs(db),
             Definition::TraitAlias(it) => it.docs(db),
-            Definition::TypeAlias(it) => it.docs(db),
+            Definition::TypeAlias(it) => {
+                it.docs(db).or_else(|| {
+                    // docs are missing, try to fall back to the docs of the aliased item.
+                    let adt = it.ty(db).as_adt()?;
+                    let docs = adt.docs(db)?;
+                    let docs = format!(
+                        "*This is the documentation for* `{}`\n\n{}",
+                        adt.display(db, edition),
+                        docs.as_str()
+                    );
+                    Some(Documentation::new(docs))
+                })
+            }
             Definition::BuiltinType(it) => {
                 famous_defs.and_then(|fd| {
                     // std exposes prim_{} modules with docstrings on the root to document the builtins
@@ -318,7 +330,8 @@ impl IdentClass {
                         .map(IdentClass::NameClass)
                         .or_else(|| NameRefClass::classify_lifetime(sema, &lifetime).map(IdentClass::NameRefClass))
                 },
-                ast::RangeExpr(range_expr) => OperatorClass::classify_range(sema, &range_expr).map(IdentClass::Operator),
+                ast::RangePat(range_pat) => OperatorClass::classify_range_pat(sema, &range_pat).map(IdentClass::Operator),
+                ast::RangeExpr(range_expr) => OperatorClass::classify_range_expr(sema, &range_expr).map(IdentClass::Operator),
                 ast::AwaitExpr(await_expr) => OperatorClass::classify_await(sema, &await_expr).map(IdentClass::Operator),
                 ast::BinExpr(bin_expr) => OperatorClass::classify_bin(sema, &bin_expr).map(IdentClass::Operator),
                 ast::IndexExpr(index_expr) => OperatorClass::classify_index(sema, &index_expr).map(IdentClass::Operator),
@@ -558,7 +571,14 @@ pub enum OperatorClass {
 }
 
 impl OperatorClass {
-    pub fn classify_range(
+    pub fn classify_range_pat(
+        sema: &Semantics<'_, RootDatabase>,
+        range_pat: &ast::RangePat,
+    ) -> Option<OperatorClass> {
+        sema.resolve_range_pat(range_pat).map(OperatorClass::Range)
+    }
+
+    pub fn classify_range_expr(
         sema: &Semantics<'_, RootDatabase>,
         range_expr: &ast::RangeExpr,
     ) -> Option<OperatorClass> {
diff --git a/src/tools/rust-analyzer/crates/ide/src/goto_definition.rs b/src/tools/rust-analyzer/crates/ide/src/goto_definition.rs
index ae3ef3e4437..1d42a66995b 100644
--- a/src/tools/rust-analyzer/crates/ide/src/goto_definition.rs
+++ b/src/tools/rust-analyzer/crates/ide/src/goto_definition.rs
@@ -98,6 +98,7 @@ pub(crate) fn goto_definition(
                     return Some(vec![x]);
                 }
             }
+
             Some(
                 IdentClass::classify_node(sema, &parent)?
                     .definitions()
@@ -460,7 +461,87 @@ mod tests {
     }
 
     #[test]
-    fn goto_def_range() {
+    fn goto_def_pat_range_to_inclusive() {
+        check_name(
+            "RangeToInclusive",
+            r#"
+//- minicore: range
+fn f(ch: char) -> bool {
+    match ch {
+        ..$0='z' => true,
+        _ => false
+    }
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn goto_def_pat_range_to() {
+        check_name(
+            "RangeTo",
+            r#"
+//- minicore: range
+fn f(ch: char) -> bool {
+    match ch {
+        .$0.'z' => true,
+        _ => false
+    }
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn goto_def_pat_range() {
+        check_name(
+            "Range",
+            r#"
+//- minicore: range
+fn f(ch: char) -> bool {
+    match ch {
+        'a'.$0.'z' => true,
+        _ => false
+    }
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn goto_def_pat_range_inclusive() {
+        check_name(
+            "RangeInclusive",
+            r#"
+//- minicore: range
+fn f(ch: char) -> bool {
+    match ch {
+        'a'..$0='z' => true,
+        _ => false
+    }
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn goto_def_pat_range_from() {
+        check_name(
+            "RangeFrom",
+            r#"
+//- minicore: range
+fn f(ch: char) -> bool {
+    match ch {
+        'a'..$0 => true,
+        _ => false
+    }
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn goto_def_expr_range() {
         check_name(
             "Range",
             r#"
@@ -471,7 +552,7 @@ let x = 0.$0.1;
     }
 
     #[test]
-    fn goto_def_range_from() {
+    fn goto_def_expr_range_from() {
         check_name(
             "RangeFrom",
             r#"
@@ -484,7 +565,7 @@ fn f(arr: &[i32]) -> &[i32] {
     }
 
     #[test]
-    fn goto_def_range_inclusive() {
+    fn goto_def_expr_range_inclusive() {
         check_name(
             "RangeInclusive",
             r#"
@@ -495,7 +576,7 @@ let x = 0.$0.=1;
     }
 
     #[test]
-    fn goto_def_range_full() {
+    fn goto_def_expr_range_full() {
         check_name(
             "RangeFull",
             r#"
@@ -508,7 +589,7 @@ fn f(arr: &[i32]) -> &[i32] {
     }
 
     #[test]
-    fn goto_def_range_to() {
+    fn goto_def_expr_range_to() {
         check_name(
             "RangeTo",
             r#"
@@ -521,7 +602,7 @@ fn f(arr: &[i32]) -> &[i32] {
     }
 
     #[test]
-    fn goto_def_range_to_inclusive() {
+    fn goto_def_expr_range_to_inclusive() {
         check_name(
             "RangeToInclusive",
             r#"
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 81397b07855..ef835f5bef8 100644
--- a/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs
+++ b/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs
@@ -9018,3 +9018,156 @@ foo!(BAR_$0);
         "#]],
     );
 }
+
+#[test]
+fn type_alias_without_docs() {
+    // Simple.
+    check(
+        r#"
+/// Docs for B
+struct B;
+
+type A$0 = B;
+"#,
+        expect![[r#"
+            *A*
+
+            ```rust
+            test
+            ```
+
+            ```rust
+            // size = 0, align = 1
+            type A = B
+            ```
+
+            ---
+
+            *This is the documentation for* `struct B`
+
+            Docs for B
+        "#]],
+    );
+
+    // Nested.
+    check(
+        r#"
+/// Docs for C
+struct C;
+
+type B = C;
+
+type A$0 = B;
+"#,
+        expect![[r#"
+            *A*
+
+            ```rust
+            test
+            ```
+
+            ```rust
+            // size = 0, align = 1
+            type A = B
+            ```
+
+            ---
+
+            *This is the documentation for* `struct C`
+
+            Docs for C
+        "#]],
+    );
+
+    // Showing the docs for aliased struct instead of intermediate type.
+    check(
+        r#"
+/// Docs for C
+struct C;
+
+/// Docs for B
+type B = C;
+
+type A$0 = B;
+"#,
+        expect![[r#"
+            *A*
+
+            ```rust
+            test
+            ```
+
+            ```rust
+            // size = 0, align = 1
+            type A = B
+            ```
+
+            ---
+
+            *This is the documentation for* `struct C`
+
+            Docs for C
+        "#]],
+    );
+
+    // No docs found.
+    check(
+        r#"
+struct C;
+
+type B = C;
+
+type A$0 = B;
+"#,
+        expect![[r#"
+            *A*
+
+            ```rust
+            test
+            ```
+
+            ```rust
+            // size = 0, align = 1
+            type A = B
+            ```
+        "#]],
+    );
+
+    // Multiple nested crate.
+    check(
+        r#"
+//- /lib.rs crate:c
+/// Docs for C
+pub struct C;
+
+//- /lib.rs crate:b deps:c
+pub use c::C;
+pub type B = C;
+
+//- /lib.rs crate:a deps:b
+pub use b::B;
+pub type A = B;
+
+//- /main.rs crate:main deps:a
+use a::A$0;
+"#,
+        expect![[r#"
+            *A*
+
+            ```rust
+            a
+            ```
+
+            ```rust
+            // size = 0, align = 1
+            pub type A = B
+            ```
+
+            ---
+
+            *This is the documentation for* `pub struct C`
+
+            Docs for C
+        "#]],
+    );
+}
diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/bin/main.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/bin/main.rs
index ecc8333503e..e872585c571 100644
--- a/src/tools/rust-analyzer/crates/rust-analyzer/src/bin/main.rs
+++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/bin/main.rs
@@ -85,7 +85,9 @@ fn actual_main() -> anyhow::Result<ExitCode> {
         flags::RustAnalyzerCmd::UnresolvedReferences(cmd) => cmd.run()?,
         flags::RustAnalyzerCmd::Ssr(cmd) => cmd.run()?,
         flags::RustAnalyzerCmd::Search(cmd) => cmd.run()?,
-        flags::RustAnalyzerCmd::Lsif(cmd) => cmd.run()?,
+        flags::RustAnalyzerCmd::Lsif(cmd) => {
+            cmd.run(&mut std::io::stdout(), Some(project_model::RustLibSource::Discover))?
+        }
         flags::RustAnalyzerCmd::Scip(cmd) => cmd.run()?,
         flags::RustAnalyzerCmd::RunTests(cmd) => cmd.run()?,
         flags::RustAnalyzerCmd::RustcTests(cmd) => cmd.run()?,
diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/cli/lsif.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/cli/lsif.rs
index ca8acf57bff..33c4f31fbee 100644
--- a/src/tools/rust-analyzer/crates/rust-analyzer/src/cli/lsif.rs
+++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/cli/lsif.rs
@@ -12,6 +12,7 @@ use load_cargo::{load_workspace, LoadCargoConfig, ProcMacroServerChoice};
 use lsp_types::lsif;
 use project_model::{CargoConfig, ProjectManifest, ProjectWorkspace, RustLibSource};
 use rustc_hash::FxHashMap;
+use stdx::format_to;
 use vfs::{AbsPathBuf, Vfs};
 
 use crate::{
@@ -21,7 +22,7 @@ use crate::{
     version::version,
 };
 
-struct LsifManager<'a> {
+struct LsifManager<'a, 'w> {
     count: i32,
     token_map: FxHashMap<TokenId, Id>,
     range_map: FxHashMap<FileRange, Id>,
@@ -30,6 +31,7 @@ struct LsifManager<'a> {
     analysis: &'a Analysis,
     db: &'a RootDatabase,
     vfs: &'a Vfs,
+    out: &'w mut dyn std::io::Write,
 }
 
 #[derive(Clone, Copy)]
@@ -41,8 +43,13 @@ impl From<Id> for lsp_types::NumberOrString {
     }
 }
 
-impl LsifManager<'_> {
-    fn new<'a>(analysis: &'a Analysis, db: &'a RootDatabase, vfs: &'a Vfs) -> LsifManager<'a> {
+impl LsifManager<'_, '_> {
+    fn new<'a, 'w>(
+        analysis: &'a Analysis,
+        db: &'a RootDatabase,
+        vfs: &'a Vfs,
+        out: &'w mut dyn std::io::Write,
+    ) -> LsifManager<'a, 'w> {
         LsifManager {
             count: 0,
             token_map: FxHashMap::default(),
@@ -52,6 +59,7 @@ impl LsifManager<'_> {
             analysis,
             db,
             vfs,
+            out,
         }
     }
 
@@ -70,9 +78,8 @@ impl LsifManager<'_> {
         self.add(lsif::Element::Edge(edge))
     }
 
-    // FIXME: support file in addition to stdout here
-    fn emit(&self, data: &str) {
-        println!("{data}");
+    fn emit(&mut self, data: &str) {
+        format_to!(self.out, "{data}\n");
     }
 
     fn get_token_id(&mut self, id: TokenId) -> Id {
@@ -272,14 +279,14 @@ impl LsifManager<'_> {
 }
 
 impl flags::Lsif {
-    pub fn run(self) -> anyhow::Result<()> {
+    pub fn run(
+        self,
+        out: &mut dyn std::io::Write,
+        sysroot: Option<RustLibSource>,
+    ) -> anyhow::Result<()> {
         let now = Instant::now();
-        let cargo_config = &CargoConfig {
-            sysroot: Some(RustLibSource::Discover),
-            all_targets: true,
-            set_test: true,
-            ..Default::default()
-        };
+        let cargo_config =
+            &CargoConfig { sysroot, all_targets: true, set_test: true, ..Default::default() };
         let no_progress = &|_| ();
         let load_cargo_config = LoadCargoConfig {
             load_out_dirs_from_check: true,
@@ -308,7 +315,7 @@ impl flags::Lsif {
 
         let si = StaticIndex::compute(&analysis, vendored_libs_config);
 
-        let mut lsif = LsifManager::new(&analysis, db, &vfs);
+        let mut lsif = LsifManager::new(&analysis, db, &vfs, out);
         lsif.add_vertex(lsif::Vertex::MetaData(lsif::MetaData {
             version: String::from("0.5.0"),
             project_root: lsp_types::Url::from_file_path(path).unwrap(),
diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/tests/slow-tests/cli.rs b/src/tools/rust-analyzer/crates/rust-analyzer/tests/slow-tests/cli.rs
new file mode 100644
index 00000000000..fba54666912
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/rust-analyzer/tests/slow-tests/cli.rs
@@ -0,0 +1,131 @@
+use expect_test::expect;
+use test_utils::skip_slow_tests;
+
+use crate::support::Project;
+
+// If you choose to change the test fixture here, please inform the ferrocene/needy maintainers by
+// opening an issue at https://github.com/ferrocene/needy as the tool relies on specific token
+// mapping behavior.
+#[test]
+fn lsif_contains_generated_constant() {
+    if skip_slow_tests() {
+        return;
+    }
+
+    let stdout = Project::with_fixture(
+        r#"
+//- /Cargo.toml
+[package]
+name = "foo"
+version = "0.0.0"
+
+//- /src/lib.rs
+#![allow(unused)]
+
+macro_rules! generate_const_from_identifier(
+    ($id:ident) => (
+        const _: () = { const $id: &str = "encoded_data"; };
+    )
+);
+
+generate_const_from_identifier!(REQ_001);
+mod tests {
+    use super::*;
+    generate_const_from_identifier!(REQ_002);
+}
+"#,
+    )
+    .root("foo")
+    .run_lsif();
+    let n = stdout.find(r#"{"id":2,"#).unwrap();
+    // the first 2 entries contain paths that are not stable
+    let stdout = &stdout[n..];
+    expect![[r#"
+        {"id":2,"type":"vertex","label":"foldingRangeResult","result":[{"startLine":2,"startCharacter":43,"endLine":6,"endCharacter":1},{"startLine":3,"startCharacter":19,"endLine":5,"endCharacter":5},{"startLine":9,"startCharacter":10,"endLine":12,"endCharacter":1}]}
+        {"id":3,"type":"edge","label":"textDocument/foldingRange","inV":2,"outV":1}
+        {"id":4,"type":"vertex","label":"range","start":{"line":0,"character":3},"end":{"line":0,"character":8}}
+        {"id":5,"type":"vertex","label":"resultSet"}
+        {"id":6,"type":"edge","label":"next","inV":5,"outV":4}
+        {"id":7,"type":"vertex","label":"range","start":{"line":2,"character":13},"end":{"line":2,"character":43}}
+        {"id":8,"type":"vertex","label":"resultSet"}
+        {"id":9,"type":"edge","label":"next","inV":8,"outV":7}
+        {"id":10,"type":"vertex","label":"range","start":{"line":8,"character":0},"end":{"line":8,"character":30}}
+        {"id":11,"type":"edge","label":"next","inV":8,"outV":10}
+        {"id":12,"type":"vertex","label":"range","start":{"line":8,"character":32},"end":{"line":8,"character":39}}
+        {"id":13,"type":"vertex","label":"resultSet"}
+        {"id":14,"type":"edge","label":"next","inV":13,"outV":12}
+        {"id":15,"type":"vertex","label":"range","start":{"line":9,"character":4},"end":{"line":9,"character":9}}
+        {"id":16,"type":"vertex","label":"resultSet"}
+        {"id":17,"type":"edge","label":"next","inV":16,"outV":15}
+        {"id":18,"type":"vertex","label":"range","start":{"line":10,"character":8},"end":{"line":10,"character":13}}
+        {"id":19,"type":"vertex","label":"resultSet"}
+        {"id":20,"type":"edge","label":"next","inV":19,"outV":18}
+        {"id":21,"type":"vertex","label":"range","start":{"line":11,"character":4},"end":{"line":11,"character":34}}
+        {"id":22,"type":"edge","label":"next","inV":8,"outV":21}
+        {"id":23,"type":"vertex","label":"range","start":{"line":11,"character":36},"end":{"line":11,"character":43}}
+        {"id":24,"type":"vertex","label":"resultSet"}
+        {"id":25,"type":"edge","label":"next","inV":24,"outV":23}
+        {"id":26,"type":"edge","label":"contains","inVs":[4,7,10,12,15,18,21,23],"outV":1}
+        {"id":27,"type":"vertex","label":"hoverResult","result":{"contents":{"kind":"markdown","value":"\n```rust\n#[allow]\n```\n\n---\n\nValid forms are:\n\n* \\#\\[allow(lint1, lint2, ..., /\\*opt\\*/ reason = \"...\")\\]"}}}
+        {"id":28,"type":"edge","label":"textDocument/hover","inV":27,"outV":5}
+        {"id":29,"type":"vertex","label":"referenceResult"}
+        {"id":30,"type":"edge","label":"textDocument/references","inV":29,"outV":5}
+        {"id":31,"type":"edge","label":"item","document":1,"property":"references","inVs":[4],"outV":29}
+        {"id":32,"type":"vertex","label":"hoverResult","result":{"contents":{"kind":"markdown","value":"\n```rust\nfoo\n```\n\n```rust\nmacro_rules! generate_const_from_identifier\n```"}}}
+        {"id":33,"type":"edge","label":"textDocument/hover","inV":32,"outV":8}
+        {"id":34,"type":"vertex","label":"packageInformation","name":"foo","manager":"cargo","version":"0.0.0"}
+        {"id":35,"type":"vertex","label":"moniker","scheme":"rust-analyzer","identifier":"foo::generate_const_from_identifier","unique":"scheme","kind":"export"}
+        {"id":36,"type":"edge","label":"packageInformation","inV":34,"outV":35}
+        {"id":37,"type":"edge","label":"moniker","inV":35,"outV":8}
+        {"id":38,"type":"vertex","label":"definitionResult"}
+        {"id":39,"type":"edge","label":"item","document":1,"inVs":[7],"outV":38}
+        {"id":40,"type":"edge","label":"textDocument/definition","inV":38,"outV":8}
+        {"id":41,"type":"vertex","label":"referenceResult"}
+        {"id":42,"type":"edge","label":"textDocument/references","inV":41,"outV":8}
+        {"id":43,"type":"edge","label":"item","document":1,"property":"definitions","inVs":[7],"outV":41}
+        {"id":44,"type":"edge","label":"item","document":1,"property":"references","inVs":[10,21],"outV":41}
+        {"id":45,"type":"vertex","label":"hoverResult","result":{"contents":{"kind":"markdown","value":"\n```rust\nfoo\n```\n\n```rust\nconst REQ_001: &str = \"encoded_data\"\n```"}}}
+        {"id":46,"type":"edge","label":"textDocument/hover","inV":45,"outV":13}
+        {"id":47,"type":"vertex","label":"moniker","scheme":"rust-analyzer","identifier":"foo::REQ_001","unique":"scheme","kind":"export"}
+        {"id":48,"type":"edge","label":"packageInformation","inV":34,"outV":47}
+        {"id":49,"type":"edge","label":"moniker","inV":47,"outV":13}
+        {"id":50,"type":"vertex","label":"definitionResult"}
+        {"id":51,"type":"edge","label":"item","document":1,"inVs":[12],"outV":50}
+        {"id":52,"type":"edge","label":"textDocument/definition","inV":50,"outV":13}
+        {"id":53,"type":"vertex","label":"referenceResult"}
+        {"id":54,"type":"edge","label":"textDocument/references","inV":53,"outV":13}
+        {"id":55,"type":"edge","label":"item","document":1,"property":"definitions","inVs":[12],"outV":53}
+        {"id":56,"type":"vertex","label":"hoverResult","result":{"contents":{"kind":"markdown","value":"\n```rust\nfoo\n```\n\n```rust\nmod tests\n```"}}}
+        {"id":57,"type":"edge","label":"textDocument/hover","inV":56,"outV":16}
+        {"id":58,"type":"vertex","label":"moniker","scheme":"rust-analyzer","identifier":"foo::tests","unique":"scheme","kind":"export"}
+        {"id":59,"type":"edge","label":"packageInformation","inV":34,"outV":58}
+        {"id":60,"type":"edge","label":"moniker","inV":58,"outV":16}
+        {"id":61,"type":"vertex","label":"definitionResult"}
+        {"id":62,"type":"edge","label":"item","document":1,"inVs":[15],"outV":61}
+        {"id":63,"type":"edge","label":"textDocument/definition","inV":61,"outV":16}
+        {"id":64,"type":"vertex","label":"referenceResult"}
+        {"id":65,"type":"edge","label":"textDocument/references","inV":64,"outV":16}
+        {"id":66,"type":"edge","label":"item","document":1,"property":"definitions","inVs":[15],"outV":64}
+        {"id":67,"type":"vertex","label":"hoverResult","result":{"contents":{"kind":"markdown","value":"\n```rust\nextern crate foo\n```"}}}
+        {"id":68,"type":"edge","label":"textDocument/hover","inV":67,"outV":19}
+        {"id":69,"type":"vertex","label":"definitionResult"}
+        {"id":70,"type":"vertex","label":"range","start":{"line":0,"character":0},"end":{"line":13,"character":0}}
+        {"id":71,"type":"edge","label":"contains","inVs":[70],"outV":1}
+        {"id":72,"type":"edge","label":"item","document":1,"inVs":[70],"outV":69}
+        {"id":73,"type":"edge","label":"textDocument/definition","inV":69,"outV":19}
+        {"id":74,"type":"vertex","label":"referenceResult"}
+        {"id":75,"type":"edge","label":"textDocument/references","inV":74,"outV":19}
+        {"id":76,"type":"edge","label":"item","document":1,"property":"references","inVs":[18],"outV":74}
+        {"id":77,"type":"vertex","label":"hoverResult","result":{"contents":{"kind":"markdown","value":"\n```rust\nfoo::tests\n```\n\n```rust\nconst REQ_002: &str = \"encoded_data\"\n```"}}}
+        {"id":78,"type":"edge","label":"textDocument/hover","inV":77,"outV":24}
+        {"id":79,"type":"vertex","label":"moniker","scheme":"rust-analyzer","identifier":"foo::tests::REQ_002","unique":"scheme","kind":"export"}
+        {"id":80,"type":"edge","label":"packageInformation","inV":34,"outV":79}
+        {"id":81,"type":"edge","label":"moniker","inV":79,"outV":24}
+        {"id":82,"type":"vertex","label":"definitionResult"}
+        {"id":83,"type":"edge","label":"item","document":1,"inVs":[23],"outV":82}
+        {"id":84,"type":"edge","label":"textDocument/definition","inV":82,"outV":24}
+        {"id":85,"type":"vertex","label":"referenceResult"}
+        {"id":86,"type":"edge","label":"textDocument/references","inV":85,"outV":24}
+        {"id":87,"type":"edge","label":"item","document":1,"property":"definitions","inVs":[23],"outV":85}
+    "#]].assert_eq(stdout);
+}
diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/tests/slow-tests/main.rs b/src/tools/rust-analyzer/crates/rust-analyzer/tests/slow-tests/main.rs
index 54cd27f4b3b..97c76bf8d17 100644
--- a/src/tools/rust-analyzer/crates/rust-analyzer/tests/slow-tests/main.rs
+++ b/src/tools/rust-analyzer/crates/rust-analyzer/tests/slow-tests/main.rs
@@ -10,6 +10,7 @@
 
 #![allow(clippy::disallowed_types)]
 
+mod cli;
 mod ratoml;
 mod support;
 mod testdir;
diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/tests/slow-tests/support.rs b/src/tools/rust-analyzer/crates/rust-analyzer/tests/slow-tests/support.rs
index 18aface632d..78572e37a9b 100644
--- a/src/tools/rust-analyzer/crates/rust-analyzer/tests/slow-tests/support.rs
+++ b/src/tools/rust-analyzer/crates/rust-analyzer/tests/slow-tests/support.rs
@@ -6,11 +6,13 @@ use std::{
 };
 
 use crossbeam_channel::{after, select, Receiver};
+use itertools::Itertools;
 use lsp_server::{Connection, Message, Notification, Request};
 use lsp_types::{notification::Exit, request::Shutdown, TextDocumentIdentifier, Url};
 use parking_lot::{Mutex, MutexGuard};
 use paths::{Utf8Path, Utf8PathBuf};
 use rust_analyzer::{
+    cli::flags,
     config::{Config, ConfigChange, ConfigErrors},
     lsp, main_loop,
 };
@@ -84,6 +86,46 @@ impl Project<'_> {
         self
     }
 
+    pub(crate) fn run_lsif(self) -> String {
+        let tmp_dir = self.tmp_dir.unwrap_or_else(|| {
+            if self.root_dir_contains_symlink {
+                TestDir::new_symlink()
+            } else {
+                TestDir::new()
+            }
+        });
+
+        let FixtureWithProjectMeta {
+            fixture,
+            mini_core,
+            proc_macro_names,
+            toolchain,
+            target_data_layout: _,
+        } = FixtureWithProjectMeta::parse(self.fixture);
+        assert!(proc_macro_names.is_empty());
+        assert!(mini_core.is_none());
+        assert!(toolchain.is_none());
+
+        for entry in fixture {
+            let path = tmp_dir.path().join(&entry.path['/'.len_utf8()..]);
+            fs::create_dir_all(path.parent().unwrap()).unwrap();
+            fs::write(path.as_path(), entry.text.as_bytes()).unwrap();
+        }
+
+        let tmp_dir_path = AbsPathBuf::assert(tmp_dir.path().to_path_buf());
+        let mut buf = Vec::new();
+        flags::Lsif::run(
+            flags::Lsif {
+                path: tmp_dir_path.join(self.roots.iter().exactly_one().unwrap()).into(),
+                exclude_vendored_libraries: false,
+            },
+            &mut buf,
+            None,
+        )
+        .unwrap();
+        String::from_utf8(buf).unwrap()
+    }
+
     pub(crate) fn server(self) -> Server {
         static CONFIG_DIR_LOCK: Mutex<()> = Mutex::new(());
         let tmp_dir = self.tmp_dir.unwrap_or_else(|| {