-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add command to report unresolved references
- Loading branch information
Showing
4 changed files
with
209 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ mod rustc_tests; | |
mod scip; | ||
mod ssr; | ||
mod symbols; | ||
mod unresolved_references; | ||
|
||
mod progress_report; | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
use hir::{ | ||
db::HirDatabase, AnyDiagnostic, Crate, HirFileIdExt as _, MacroFileIdExt 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 project_model::{CargoConfig, RustLibSource}; | ||
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 cargo_config = | ||
CargoConfig { sysroot: Some(RustLibSource::Discover), ..Default::default() }; | ||
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 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, 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, | ||
file_id: FileId, | ||
module: &Module, | ||
) -> Vec<TextRange> { | ||
let mut unresolved_references = all_unresolved_references(db, 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; | ||
|
||
if node.file_id != file_id { | ||
continue; | ||
} | ||
|
||
unresolved_references.retain(|range| !node.value.text_range().contains_range(*range)); | ||
} | ||
|
||
unresolved_references | ||
} | ||
|
||
fn all_unresolved_references(db: &RootDatabase, file_id: FileId) -> Vec<TextRange> { | ||
let sema = Semantics::new(db); | ||
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(ast::NameLike::NameRef(name_ref)) = ast::NameLike::cast(syntax) else { | ||
continue; | ||
}; | ||
|
||
// if we can classify the name_ref, it's not unresolved | ||
if NameRefClass::classify(&sema, &name_ref).is_some() { | ||
continue; | ||
} | ||
|
||
// if we couldn't classify it, try descending into macros and classifying that | ||
let Some(ast::NameLike::NameRef(descended_name_ref)) = name_ref | ||
.Self_token() | ||
.or_else(|| name_ref.crate_token()) | ||
.or_else(|| name_ref.ident_token()) | ||
.or_else(|| name_ref.int_number_token()) | ||
.or_else(|| name_ref.self_token()) | ||
.or_else(|| name_ref.super_token()) | ||
.and_then(|token| { | ||
sema.descend_into_macros_single_exact(token).parent().and_then(ast::NameLike::cast) | ||
}) | ||
else { | ||
continue; | ||
}; | ||
|
||
if NameRefClass::classify(&sema, &descended_name_ref).is_some() { | ||
continue; | ||
} | ||
|
||
// if we still couldn't classify it, check if it's TODO | ||
if descended_name_ref.syntax().ancestors().any(|it| it.kind() == SyntaxKind::ATTR) | ||
&& !sema | ||
.hir_file_for(descended_name_ref.syntax()) | ||
.macro_file() | ||
.map_or(false, |it| it.is_derive_attr_pseudo_expansion(sema.db)) | ||
{ | ||
continue; | ||
} | ||
|
||
// otherwise, it's unresolved | ||
unresolved_references.push(name_ref.syntax().text_range()); | ||
} | ||
unresolved_references | ||
} |