about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2024-09-11 10:19:30 +0000
committerbors <bors@rust-lang.org>2024-09-11 10:19:30 +0000
commitec72a9978aeeb66d63fca21c5352aa44641427d9 (patch)
tree132d73aaad36e104c2724184f594a18a50d55c98
parent0233b774993fd39ae3a95d5485b61ef37bf25975 (diff)
parentf44bdb5c78a726da1960856c973907de1eb68aec (diff)
downloadrust-ec72a9978aeeb66d63fca21c5352aa44641427d9.tar.gz
rust-ec72a9978aeeb66d63fca21c5352aa44641427d9.zip
Auto merge of #17904 - darichey:unresolved-references, r=Veykril
Add command to report unresolved references

Adds `rust-analyzer unresolved-references` which reports unresolved references. This is useful for debugging and regression testing for both rust-analyzer and project generators like Buck's rust-project.

As discussed: https://rust-lang.zulipchat.com/#narrow/stream/185405-t-compiler.2Frust-analyzer/topic/Command.20to.20report.20unresolved.20references
-rw-r--r--src/tools/rust-analyzer/crates/rust-analyzer/src/bin/main.rs1
-rw-r--r--src/tools/rust-analyzer/crates/rust-analyzer/src/cli.rs1
-rw-r--r--src/tools/rust-analyzer/crates/rust-analyzer/src/cli/flags.rs23
-rw-r--r--src/tools/rust-analyzer/crates/rust-analyzer/src/cli/unresolved_references.rs175
4 files changed, 200 insertions, 0 deletions
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 21b481c1fa2..41b42573f08 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
@@ -82,6 +82,7 @@ fn actual_main() -> anyhow::Result<ExitCode> {
         flags::RustAnalyzerCmd::Highlight(cmd) => cmd.run()?,
         flags::RustAnalyzerCmd::AnalysisStats(cmd) => cmd.run(verbosity)?,
         flags::RustAnalyzerCmd::Diagnostics(cmd) => cmd.run()?,
+        flags::RustAnalyzerCmd::UnresolvedReferences(cmd) => cmd.run()?,
         flags::RustAnalyzerCmd::Ssr(cmd) => cmd.run()?,
         flags::RustAnalyzerCmd::Search(cmd) => cmd.run()?,
         flags::RustAnalyzerCmd::Lsif(cmd) => cmd.run()?,
diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/cli.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/cli.rs
index 5eb6ff664f6..a7ec5af89fc 100644
--- a/src/tools/rust-analyzer/crates/rust-analyzer/src/cli.rs
+++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/cli.rs
@@ -13,6 +13,7 @@ mod rustc_tests;
 mod scip;
 mod ssr;
 mod symbols;
+mod unresolved_references;
 
 mod progress_report;
 
diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/cli/flags.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/cli/flags.rs
index 16d90de661a..73e71658d17 100644
--- a/src/tools/rust-analyzer/crates/rust-analyzer/src/cli/flags.rs
+++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/cli/flags.rs
@@ -124,6 +124,19 @@ xflags::xflags! {
             optional --proc-macro-srv path: PathBuf
         }
 
+        /// Report unresolved references
+        cmd unresolved-references {
+            /// Directory with Cargo.toml.
+            required path: PathBuf
+
+            /// Don't run build scripts or load `OUT_DIR` values by running `cargo check` before analysis.
+            optional --disable-build-scripts
+            /// Don't use expand proc macros.
+            optional --disable-proc-macros
+            /// Run a custom proc-macro-srv binary.
+            optional --proc-macro-srv path: PathBuf
+        }
+
         cmd ssr {
             /// A structured search replace rule (`$a.foo($b) ==>> bar($a, $b)`)
             repeated rule: SsrRule
@@ -181,6 +194,7 @@ pub enum RustAnalyzerCmd {
     RunTests(RunTests),
     RustcTests(RustcTests),
     Diagnostics(Diagnostics),
+    UnresolvedReferences(UnresolvedReferences),
     Ssr(Ssr),
     Search(Search),
     Lsif(Lsif),
@@ -251,6 +265,15 @@ pub struct Diagnostics {
 }
 
 #[derive(Debug)]
+pub struct UnresolvedReferences {
+    pub path: PathBuf,
+
+    pub disable_build_scripts: bool,
+    pub disable_proc_macros: bool,
+    pub proc_macro_srv: Option<PathBuf>,
+}
+
+#[derive(Debug)]
 pub struct Ssr {
     pub rule: Vec<SsrRule>,
 }
diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/cli/unresolved_references.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/cli/unresolved_references.rs
new file mode 100644
index 00000000000..986bd018b42
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/cli/unresolved_references.rs
@@ -0,0 +1,175 @@
+//! Reports references in code that the IDE layer cannot resolve.
+use hir::{db::HirDatabase, AnyDiagnostic, Crate, HirFileIdExt as _, Module, Semantics};
+use ide::{AnalysisHost, RootDatabase, TextRange};
+use ide_db::{
+    base_db::{SourceDatabase, SourceRootDatabase},
+    defs::NameRefClass,
+    EditionedFileId, FxHashSet, LineIndexDatabase as _,
+};
+use load_cargo::{load_workspace_at, LoadCargoConfig, ProcMacroServerChoice};
+use parser::SyntaxKind;
+use syntax::{ast, AstNode, WalkEvent};
+use vfs::FileId;
+
+use crate::cli::flags;
+
+impl flags::UnresolvedReferences {
+    pub fn run(self) -> anyhow::Result<()> {
+        const STACK_SIZE: usize = 1024 * 1024 * 8;
+
+        let handle = stdx::thread::Builder::new(stdx::thread::ThreadIntent::LatencySensitive)
+            .name("BIG_STACK_THREAD".into())
+            .stack_size(STACK_SIZE)
+            .spawn(|| self.run_())
+            .unwrap();
+
+        handle.join()
+    }
+
+    fn run_(self) -> anyhow::Result<()> {
+        let root =
+            vfs::AbsPathBuf::assert_utf8(std::env::current_dir()?.join(&self.path)).normalize();
+        let config = crate::config::Config::new(
+            root.clone(),
+            lsp_types::ClientCapabilities::default(),
+            vec![],
+            None,
+        );
+        let cargo_config = config.cargo(None);
+        let with_proc_macro_server = if let Some(p) = &self.proc_macro_srv {
+            let path = vfs::AbsPathBuf::assert_utf8(std::env::current_dir()?.join(p));
+            ProcMacroServerChoice::Explicit(path)
+        } else {
+            ProcMacroServerChoice::Sysroot
+        };
+        let load_cargo_config = LoadCargoConfig {
+            load_out_dirs_from_check: !self.disable_build_scripts,
+            with_proc_macro_server,
+            prefill_caches: false,
+        };
+        let (db, vfs, _proc_macro) =
+            load_workspace_at(&self.path, &cargo_config, &load_cargo_config, &|_| {})?;
+        let host = AnalysisHost::with_database(db);
+        let db = host.raw_database();
+        let sema = Semantics::new(db);
+
+        let mut visited_files = FxHashSet::default();
+
+        let work = all_modules(db).into_iter().filter(|module| {
+            let file_id = module.definition_source_file_id(db).original_file(db);
+            let source_root = db.file_source_root(file_id.into());
+            let source_root = db.source_root(source_root);
+            !source_root.is_library
+        });
+
+        for module in work {
+            let file_id = module.definition_source_file_id(db).original_file(db);
+            if !visited_files.contains(&file_id) {
+                let crate_name =
+                    module.krate().display_name(db).as_deref().unwrap_or("unknown").to_owned();
+                let file_path = vfs.file_path(file_id.into());
+                eprintln!("processing crate: {crate_name}, module: {file_path}",);
+
+                let line_index = db.line_index(file_id.into());
+                let file_text = db.file_text(file_id.into());
+
+                for range in find_unresolved_references(db, &sema, file_id.into(), &module) {
+                    let line_col = line_index.line_col(range.start());
+                    let line = line_col.line + 1;
+                    let col = line_col.col + 1;
+                    let text = &file_text[range];
+                    println!("{file_path}:{line}:{col}: {text}");
+                }
+
+                visited_files.insert(file_id);
+            }
+        }
+
+        eprintln!();
+        eprintln!("scan complete");
+
+        Ok(())
+    }
+}
+
+fn all_modules(db: &dyn HirDatabase) -> Vec<Module> {
+    let mut worklist: Vec<_> =
+        Crate::all(db).into_iter().map(|krate| krate.root_module()).collect();
+    let mut modules = Vec::new();
+
+    while let Some(module) = worklist.pop() {
+        modules.push(module);
+        worklist.extend(module.children(db));
+    }
+
+    modules
+}
+
+fn find_unresolved_references(
+    db: &RootDatabase,
+    sema: &Semantics<'_, RootDatabase>,
+    file_id: FileId,
+    module: &Module,
+) -> Vec<TextRange> {
+    let mut unresolved_references = all_unresolved_references(sema, file_id);
+
+    // remove unresolved references which are within inactive code
+    let mut diagnostics = Vec::new();
+    module.diagnostics(db, &mut diagnostics, false);
+    for diagnostic in diagnostics {
+        let AnyDiagnostic::InactiveCode(inactive_code) = diagnostic else {
+            continue;
+        };
+
+        let node = inactive_code.node;
+        let range = node.map(|it| it.text_range()).original_node_file_range_rooted(db);
+
+        if range.file_id != file_id {
+            continue;
+        }
+
+        unresolved_references.retain(|r| !range.range.contains_range(*r));
+    }
+
+    unresolved_references
+}
+
+fn all_unresolved_references(
+    sema: &Semantics<'_, RootDatabase>,
+    file_id: FileId,
+) -> Vec<TextRange> {
+    let file_id = sema
+        .attach_first_edition(file_id)
+        .unwrap_or_else(|| EditionedFileId::current_edition(file_id));
+    let file = sema.parse(file_id);
+    let root = file.syntax();
+
+    let mut unresolved_references = Vec::new();
+    for event in root.preorder() {
+        let WalkEvent::Enter(syntax) = event else {
+            continue;
+        };
+        let Some(name_ref) = ast::NameRef::cast(syntax) else {
+            continue;
+        };
+        let Some(descended_name_ref) = name_ref.syntax().first_token().and_then(|tok| {
+            sema.descend_into_macros_single_exact(tok).parent().and_then(ast::NameRef::cast)
+        }) else {
+            continue;
+        };
+
+        // if we can classify the name_ref, it's not unresolved
+        if NameRefClass::classify(sema, &descended_name_ref).is_some() {
+            continue;
+        }
+
+        // if we couldn't classify it, but it's in an attr, ignore it. See #10935
+        if descended_name_ref.syntax().ancestors().any(|it| it.kind() == SyntaxKind::ATTR) {
+            continue;
+        }
+
+        // otherwise, it's unresolved
+        unresolved_references.push(name_ref.syntax().text_range());
+    }
+    unresolved_references
+}