1 //! Fully type-check project and print various stats, like the number of type
2 //! errors.
3 
4 use std::{
5     env,
6     time::{SystemTime, UNIX_EPOCH},
7 };
8 
9 use hir::{
10     db::{AstDatabase, DefDatabase, HirDatabase},
11     AssocItem, Crate, Function, HasSource, HirDisplay, ModuleDef,
12 };
13 use hir_def::{body::BodySourceMap, expr::ExprId, FunctionId};
14 use hir_ty::{TyExt, TypeWalk};
15 use ide::{Analysis, AnalysisHost, LineCol, RootDatabase};
16 use ide_db::base_db::{
17     salsa::{self, debug::DebugQueryTable, ParallelDatabase},
18     SourceDatabase, SourceDatabaseExt,
19 };
20 use itertools::Itertools;
21 use oorandom::Rand32;
22 use profile::{Bytes, StopWatch};
23 use project_model::{CargoConfig, ProjectManifest, ProjectWorkspace};
24 use rayon::prelude::*;
25 use rustc_hash::FxHashSet;
26 use stdx::format_to;
27 use syntax::{AstNode, SyntaxNode};
28 use vfs::{AbsPathBuf, Vfs, VfsPath};
29 
30 use crate::cli::{
31     flags,
32     load_cargo::{load_workspace, LoadCargoConfig},
33     print_memory_usage,
34     progress_report::ProgressReport,
35     report_metric, Result, Verbosity,
36 };
37 
38 /// Need to wrap Snapshot to provide `Clone` impl for `map_with`
39 struct Snap<DB>(DB);
40 impl<DB: ParallelDatabase> Clone for Snap<salsa::Snapshot<DB>> {
clone(&self) -> Snap<salsa::Snapshot<DB>>41     fn clone(&self) -> Snap<salsa::Snapshot<DB>> {
42         Snap(self.0.snapshot())
43     }
44 }
45 
46 impl flags::AnalysisStats {
run(self, verbosity: Verbosity) -> Result<()>47     pub fn run(self, verbosity: Verbosity) -> Result<()> {
48         let mut rng = {
49             let seed = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64;
50             Rand32::new(seed)
51         };
52 
53         let mut cargo_config = CargoConfig::default();
54         cargo_config.no_sysroot = self.no_sysroot;
55         let load_cargo_config = LoadCargoConfig {
56             load_out_dirs_from_check: !self.disable_build_scripts,
57             with_proc_macro: !self.disable_proc_macros,
58             prefill_caches: false,
59         };
60         let no_progress = &|_| ();
61 
62         let mut db_load_sw = self.stop_watch();
63 
64         let path = AbsPathBuf::assert(env::current_dir()?.join(&self.path));
65         let manifest = ProjectManifest::discover_single(&path)?;
66 
67         let mut workspace = ProjectWorkspace::load(manifest, &cargo_config, no_progress)?;
68         let metadata_time = db_load_sw.elapsed();
69 
70         let build_scripts_time = if self.disable_build_scripts {
71             None
72         } else {
73             let mut build_scripts_sw = self.stop_watch();
74             let bs = workspace.run_build_scripts(&cargo_config, no_progress)?;
75             workspace.set_build_scripts(bs);
76             Some(build_scripts_sw.elapsed())
77         };
78 
79         let (host, vfs, _proc_macro) = load_workspace(workspace, &load_cargo_config)?;
80         let db = host.raw_database();
81         eprint!("{:<20} {}", "Database loaded:", db_load_sw.elapsed());
82         eprint!(" (metadata {}", metadata_time);
83         if let Some(build_scripts_time) = build_scripts_time {
84             eprint!("; build {}", build_scripts_time);
85         }
86         eprintln!(")");
87 
88         let mut analysis_sw = self.stop_watch();
89         let mut num_crates = 0;
90         let mut visited_modules = FxHashSet::default();
91         let mut visit_queue = Vec::new();
92 
93         let mut krates = Crate::all(db);
94         if self.randomize {
95             shuffle(&mut rng, &mut krates);
96         }
97         for krate in krates {
98             let module = krate.root_module(db);
99             let file_id = module.definition_source(db).file_id;
100             let file_id = file_id.original_file(db);
101             let source_root = db.file_source_root(file_id);
102             let source_root = db.source_root(source_root);
103             if !source_root.is_library || self.with_deps {
104                 num_crates += 1;
105                 visit_queue.push(module);
106             }
107         }
108 
109         if self.randomize {
110             shuffle(&mut rng, &mut visit_queue);
111         }
112 
113         eprint!("  crates: {}", num_crates);
114         let mut num_decls = 0;
115         let mut funcs = Vec::new();
116         while let Some(module) = visit_queue.pop() {
117             if visited_modules.insert(module) {
118                 visit_queue.extend(module.children(db));
119 
120                 for decl in module.declarations(db) {
121                     num_decls += 1;
122                     if let ModuleDef::Function(f) = decl {
123                         funcs.push(f);
124                     }
125                 }
126 
127                 for impl_def in module.impl_defs(db) {
128                     for item in impl_def.items(db) {
129                         num_decls += 1;
130                         if let AssocItem::Function(f) = item {
131                             funcs.push(f);
132                         }
133                     }
134                 }
135             }
136         }
137         eprintln!(", mods: {}, decls: {}, fns: {}", visited_modules.len(), num_decls, funcs.len());
138         eprintln!("{:<20} {}", "Item Collection:", analysis_sw.elapsed());
139 
140         if self.randomize {
141             shuffle(&mut rng, &mut funcs);
142         }
143 
144         if !self.skip_inference {
145             self.run_inference(&host, db, &vfs, &funcs, verbosity);
146         }
147 
148         let total_span = analysis_sw.elapsed();
149         eprintln!("{:<20} {}", "Total:", total_span);
150         report_metric("total time", total_span.time.as_millis() as u64, "ms");
151         if let Some(instructions) = total_span.instructions {
152             report_metric("total instructions", instructions, "#instr");
153         }
154         if let Some(memory) = total_span.memory {
155             report_metric("total memory", memory.allocated.megabytes() as u64, "MB");
156         }
157 
158         if env::var("RA_COUNT").is_ok() {
159             eprintln!("{}", profile::countme::get_all());
160         }
161 
162         if self.source_stats {
163             let mut total_file_size = Bytes::default();
164             for e in ide_db::base_db::ParseQuery.in_db(db).entries::<Vec<_>>() {
165                 total_file_size += syntax_len(db.parse(e.key).syntax_node())
166             }
167 
168             let mut total_macro_file_size = Bytes::default();
169             for e in hir::db::ParseMacroExpansionQuery.in_db(db).entries::<Vec<_>>() {
170                 if let Some((val, _)) = db.parse_macro_expansion(e.key).value {
171                     total_macro_file_size += syntax_len(val.syntax_node())
172                 }
173             }
174             eprintln!("source files: {}, macro files: {}", total_file_size, total_macro_file_size);
175         }
176 
177         if self.memory_usage && verbosity.is_verbose() {
178             print_memory_usage(host, vfs);
179         }
180 
181         Ok(())
182     }
183 
run_inference( &self, host: &AnalysisHost, db: &RootDatabase, vfs: &Vfs, funcs: &[Function], verbosity: Verbosity, )184     fn run_inference(
185         &self,
186         host: &AnalysisHost,
187         db: &RootDatabase,
188         vfs: &Vfs,
189         funcs: &[Function],
190         verbosity: Verbosity,
191     ) {
192         let mut bar = match verbosity {
193             Verbosity::Quiet | Verbosity::Spammy => ProgressReport::hidden(),
194             _ if self.parallel => ProgressReport::hidden(),
195             _ => ProgressReport::new(funcs.len() as u64),
196         };
197 
198         if self.parallel {
199             let mut inference_sw = self.stop_watch();
200             let snap = Snap(db.snapshot());
201             funcs
202                 .par_iter()
203                 .map_with(snap, |snap, &f| {
204                     let f_id = FunctionId::from(f);
205                     snap.0.body(f_id.into());
206                     snap.0.infer(f_id.into());
207                 })
208                 .count();
209             eprintln!("{:<20} {}", "Parallel Inference:", inference_sw.elapsed());
210         }
211 
212         let mut inference_sw = self.stop_watch();
213         bar.tick();
214         let mut num_exprs = 0;
215         let mut num_exprs_unknown = 0;
216         let mut num_exprs_partially_unknown = 0;
217         let mut num_type_mismatches = 0;
218         let analysis = host.analysis();
219         for f in funcs.iter().copied() {
220             let name = f.name(db);
221             let full_name = f
222                 .module(db)
223                 .path_to_root(db)
224                 .into_iter()
225                 .rev()
226                 .filter_map(|it| it.name(db))
227                 .chain(Some(f.name(db)))
228                 .join("::");
229             if let Some(only_name) = self.only.as_deref() {
230                 if name.to_string() != only_name && full_name != only_name {
231                     continue;
232                 }
233             }
234             let mut msg = format!("processing: {}", full_name);
235             if verbosity.is_verbose() {
236                 if let Some(src) = f.source(db) {
237                     let original_file = src.file_id.original_file(db);
238                     let path = vfs.file_path(original_file);
239                     let syntax_range = src.value.syntax().text_range();
240                     format_to!(msg, " ({} {:?})", path, syntax_range);
241                 }
242             }
243             if verbosity.is_spammy() {
244                 bar.println(msg.to_string());
245             }
246             bar.set_message(&msg);
247             let f_id = FunctionId::from(f);
248             let (body, sm) = db.body_with_source_map(f_id.into());
249             let inference_result = db.infer(f_id.into());
250             let (previous_exprs, previous_unknown, previous_partially_unknown) =
251                 (num_exprs, num_exprs_unknown, num_exprs_partially_unknown);
252             for (expr_id, _) in body.exprs.iter() {
253                 let ty = &inference_result[expr_id];
254                 num_exprs += 1;
255                 if ty.is_unknown() {
256                     num_exprs_unknown += 1;
257                     if verbosity.is_spammy() {
258                         if let Some((path, start, end)) =
259                             expr_syntax_range(db, &analysis, vfs, &sm, expr_id)
260                         {
261                             bar.println(format!(
262                                 "{} {}:{}-{}:{}: Unknown type",
263                                 path,
264                                 start.line + 1,
265                                 start.col,
266                                 end.line + 1,
267                                 end.col,
268                             ));
269                         } else {
270                             bar.println(format!("{}: Unknown type", name,));
271                         }
272                     }
273                 } else {
274                     let mut is_partially_unknown = false;
275                     ty.walk(&mut |ty| {
276                         if ty.is_unknown() {
277                             is_partially_unknown = true;
278                         }
279                     });
280                     if is_partially_unknown {
281                         num_exprs_partially_unknown += 1;
282                     }
283                 }
284                 if self.only.is_some() && verbosity.is_spammy() {
285                     // in super-verbose mode for just one function, we print every single expression
286                     if let Some((_, start, end)) =
287                         expr_syntax_range(db, &analysis, vfs, &sm, expr_id)
288                     {
289                         bar.println(format!(
290                             "{}:{}-{}:{}: {}",
291                             start.line + 1,
292                             start.col,
293                             end.line + 1,
294                             end.col,
295                             ty.display(db)
296                         ));
297                     } else {
298                         bar.println(format!("unknown location: {}", ty.display(db)));
299                     }
300                 }
301                 if let Some(mismatch) = inference_result.type_mismatch_for_expr(expr_id) {
302                     num_type_mismatches += 1;
303                     if verbosity.is_verbose() {
304                         if let Some((path, start, end)) =
305                             expr_syntax_range(db, &analysis, vfs, &sm, expr_id)
306                         {
307                             bar.println(format!(
308                                 "{} {}:{}-{}:{}: Expected {}, got {}",
309                                 path,
310                                 start.line + 1,
311                                 start.col,
312                                 end.line + 1,
313                                 end.col,
314                                 mismatch.expected.display(db),
315                                 mismatch.actual.display(db)
316                             ));
317                         } else {
318                             bar.println(format!(
319                                 "{}: Expected {}, got {}",
320                                 name,
321                                 mismatch.expected.display(db),
322                                 mismatch.actual.display(db)
323                             ));
324                         }
325                     }
326                 }
327             }
328             if verbosity.is_spammy() {
329                 bar.println(format!(
330                     "In {}: {} exprs, {} unknown, {} partial",
331                     full_name,
332                     num_exprs - previous_exprs,
333                     num_exprs_unknown - previous_unknown,
334                     num_exprs_partially_unknown - previous_partially_unknown
335                 ));
336             }
337             bar.inc(1);
338         }
339 
340         bar.finish_and_clear();
341         eprintln!(
342             "  exprs: {}, ??ty: {} ({}%), ?ty: {} ({}%), !ty: {}",
343             num_exprs,
344             num_exprs_unknown,
345             percentage(num_exprs_unknown, num_exprs),
346             num_exprs_partially_unknown,
347             percentage(num_exprs_partially_unknown, num_exprs),
348             num_type_mismatches
349         );
350         report_metric("unknown type", num_exprs_unknown, "#");
351         report_metric("type mismatches", num_type_mismatches, "#");
352 
353         eprintln!("{:<20} {}", "Inference:", inference_sw.elapsed());
354     }
355 
stop_watch(&self) -> StopWatch356     fn stop_watch(&self) -> StopWatch {
357         StopWatch::start().memory(self.memory_usage)
358     }
359 }
360 
expr_syntax_range( db: &RootDatabase, analysis: &Analysis, vfs: &Vfs, sm: &BodySourceMap, expr_id: ExprId, ) -> Option<(VfsPath, LineCol, LineCol)>361 fn expr_syntax_range(
362     db: &RootDatabase,
363     analysis: &Analysis,
364     vfs: &Vfs,
365     sm: &BodySourceMap,
366     expr_id: ExprId,
367 ) -> Option<(VfsPath, LineCol, LineCol)> {
368     let src = sm.expr_syntax(expr_id);
369     if let Ok(src) = src {
370         let root = db.parse_or_expand(src.file_id).unwrap();
371         let node = src.map(|e| e.to_node(&root).syntax().clone());
372         let original_range = node.as_ref().original_file_range(db);
373         let path = vfs.file_path(original_range.file_id);
374         let line_index = analysis.file_line_index(original_range.file_id).unwrap();
375         let text_range = original_range.range;
376         let (start, end) =
377             (line_index.line_col(text_range.start()), line_index.line_col(text_range.end()));
378         Some((path, start, end))
379     } else {
380         None
381     }
382 }
383 
shuffle<T>(rng: &mut Rand32, slice: &mut [T])384 fn shuffle<T>(rng: &mut Rand32, slice: &mut [T]) {
385     for i in 0..slice.len() {
386         randomize_first(rng, &mut slice[i..]);
387     }
388 
389     fn randomize_first<T>(rng: &mut Rand32, slice: &mut [T]) {
390         assert!(!slice.is_empty());
391         let idx = rng.rand_range(0..slice.len() as u32) as usize;
392         slice.swap(0, idx);
393     }
394 }
395 
percentage(n: u64, total: u64) -> u64396 fn percentage(n: u64, total: u64) -> u64 {
397     (n * 100).checked_div(total).unwrap_or(100)
398 }
399 
syntax_len(node: SyntaxNode) -> usize400 fn syntax_len(node: SyntaxNode) -> usize {
401     // Macro expanded code doesn't contain whitespace, so erase *all* whitespace
402     // to make macro and non-macro code comparable.
403     node.to_string().replace(|it: char| it.is_ascii_whitespace(), "").len()
404 }
405