Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add command to report unresolved references #17904

Merged
merged 1 commit into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/rust-analyzer/src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()?,
Expand Down
1 change: 1 addition & 0 deletions crates/rust-analyzer/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod rustc_tests;
mod scip;
mod ssr;
mod symbols;
mod unresolved_references;

mod progress_report;

Expand Down
23 changes: 23 additions & 0 deletions crates/rust-analyzer/src/cli/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -181,6 +194,7 @@ pub enum RustAnalyzerCmd {
RunTests(RunTests),
RustcTests(RustcTests),
Diagnostics(Diagnostics),
UnresolvedReferences(UnresolvedReferences),
Ssr(Ssr),
Search(Search),
Lsif(Lsif),
Expand Down Expand Up @@ -250,6 +264,15 @@ pub struct Diagnostics {
pub proc_macro_srv: Option<PathBuf>,
}

#[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>,
Expand Down
175 changes: 175 additions & 0 deletions crates/rust-analyzer/src/cli/unresolved_references.rs
Original file line number Diff line number Diff line change
@@ -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
}