Skip to main content

ra_ap_rust_analyzer/cli/
unresolved_references.rs

1//! Reports references in code that the IDE layer cannot resolve.
2use hir::{AnyDiagnostic, Crate, Module, Semantics, db::HirDatabase, sym};
3use ide::{AnalysisHost, RootDatabase, TextRange};
4use ide_db::{FxHashSet, LineIndexDatabase as _, base_db::SourceDatabase, defs::NameRefClass};
5use load_cargo::{LoadCargoConfig, ProcMacroServerChoice, load_workspace_at};
6use parser::SyntaxKind;
7use syntax::{AstNode, WalkEvent, ast};
8use vfs::FileId;
9
10use crate::cli::flags;
11
12impl flags::UnresolvedReferences {
13    pub fn run(self) -> anyhow::Result<()> {
14        const STACK_SIZE: usize = 1024 * 1024 * 8;
15
16        let handle = stdx::thread::Builder::new(
17            stdx::thread::ThreadIntent::LatencySensitive,
18            "BIG_STACK_THREAD",
19        )
20        .stack_size(STACK_SIZE)
21        .spawn(|| self.run_())
22        .unwrap();
23
24        handle.join()
25    }
26
27    fn run_(self) -> anyhow::Result<()> {
28        let root =
29            vfs::AbsPathBuf::assert_utf8(std::env::current_dir()?.join(&self.path)).normalize();
30        let config = crate::config::Config::new(
31            root,
32            lsp_types::ClientCapabilities::default(),
33            vec![],
34            None,
35        );
36        let cargo_config = config.cargo(None);
37        let with_proc_macro_server = if let Some(p) = &self.proc_macro_srv {
38            let path = vfs::AbsPathBuf::assert_utf8(std::env::current_dir()?.join(p));
39            ProcMacroServerChoice::Explicit(path)
40        } else {
41            ProcMacroServerChoice::Sysroot
42        };
43        let load_cargo_config = LoadCargoConfig {
44            load_out_dirs_from_check: !self.disable_build_scripts,
45            with_proc_macro_server,
46            prefill_caches: false,
47            proc_macro_processes: config.proc_macro_num_processes(),
48        };
49        let (db, vfs, _proc_macro) =
50            load_workspace_at(&self.path, &cargo_config, &load_cargo_config, &|_| {})?;
51        let host = AnalysisHost::with_database(db);
52        let db = host.raw_database();
53        let sema = Semantics::new(db);
54
55        let mut visited_files = FxHashSet::default();
56
57        let work = all_modules(db).into_iter().filter(|module| {
58            let file_id = module.definition_source_file_id(db).original_file(db);
59            let source_root = db.file_source_root(file_id.file_id(db)).source_root_id(db);
60            let source_root = db.source_root(source_root).source_root(db);
61            !source_root.is_library
62        });
63
64        for module in work {
65            let file_id = module.definition_source_file_id(db).original_file(db);
66            let file_id = file_id.file_id(db);
67            if !visited_files.contains(&file_id) {
68                let crate_name = module
69                    .krate(db)
70                    .display_name(db)
71                    .as_deref()
72                    .unwrap_or(&sym::unknown)
73                    .to_owned();
74                let file_path = vfs.file_path(file_id);
75                eprintln!("processing crate: {crate_name}, module: {file_path}",);
76
77                let line_index = db.line_index(file_id);
78                let file_text = db.file_text(file_id);
79
80                for range in find_unresolved_references(db, &sema, file_id, &module) {
81                    let line_col = line_index.line_col(range.start());
82                    let line = line_col.line + 1;
83                    let col = line_col.col + 1;
84                    let text = &file_text.text(db)[range];
85                    println!("{file_path}:{line}:{col}: {text}");
86                }
87
88                visited_files.insert(file_id);
89            }
90        }
91
92        eprintln!();
93        eprintln!("scan complete");
94
95        Ok(())
96    }
97}
98
99fn all_modules(db: &dyn HirDatabase) -> Vec<Module> {
100    let mut worklist: Vec<_> =
101        Crate::all(db).into_iter().map(|krate| krate.root_module(db)).collect();
102    let mut modules = Vec::new();
103
104    while let Some(module) = worklist.pop() {
105        modules.push(module);
106        worklist.extend(module.children(db));
107    }
108
109    modules
110}
111
112fn find_unresolved_references(
113    db: &RootDatabase,
114    sema: &Semantics<'_, RootDatabase>,
115    file_id: FileId,
116    module: &Module,
117) -> Vec<TextRange> {
118    let mut unresolved_references = all_unresolved_references(sema, file_id);
119
120    // remove unresolved references which are within inactive code
121    let mut diagnostics = Vec::new();
122    module.diagnostics(db, &mut diagnostics, false);
123    for diagnostic in diagnostics {
124        let AnyDiagnostic::InactiveCode(inactive_code) = diagnostic else {
125            continue;
126        };
127
128        let node = inactive_code.node;
129        let range = node.map(|it| it.text_range()).original_node_file_range_rooted(db);
130
131        if range.file_id.file_id(db) != file_id {
132            continue;
133        }
134
135        unresolved_references.retain(|r| !range.range.contains_range(*r));
136    }
137
138    unresolved_references
139}
140
141fn all_unresolved_references(
142    sema: &Semantics<'_, RootDatabase>,
143    file_id: FileId,
144) -> Vec<TextRange> {
145    let file_id = sema.attach_first_edition(file_id);
146    let file = sema.parse(file_id);
147    let root = file.syntax();
148
149    let mut unresolved_references = Vec::new();
150    for event in root.preorder() {
151        let WalkEvent::Enter(syntax) = event else {
152            continue;
153        };
154        let Some(name_ref) = ast::NameRef::cast(syntax) else {
155            continue;
156        };
157        let Some(descended_name_ref) = name_ref.syntax().first_token().and_then(|tok| {
158            sema.descend_into_macros_single_exact(tok).parent().and_then(ast::NameRef::cast)
159        }) else {
160            continue;
161        };
162
163        // if we can classify the name_ref, it's not unresolved
164        if NameRefClass::classify(sema, &descended_name_ref).is_some() {
165            continue;
166        }
167
168        // if we couldn't classify it, but it's in an attr, ignore it. See #10935
169        if descended_name_ref.syntax().ancestors().any(|it| it.kind() == SyntaxKind::ATTR) {
170            continue;
171        }
172
173        // otherwise, it's unresolved
174        unresolved_references.push(name_ref.syntax().text_range());
175    }
176    unresolved_references
177}