1use super::decorators;
7use super::types::{FixtureDefinition, FixtureUsage, UndeclaredFixture};
8use super::FixtureDatabase;
9use rustpython_parser::ast::{ArgWithDefault, Arguments, Expr, Stmt};
10use rustpython_parser::{parse, Mode};
11use std::collections::{HashMap, HashSet};
12use std::path::{Path, PathBuf};
13use tracing::{debug, error, info};
14
15impl FixtureDatabase {
16 pub fn analyze_file(&self, file_path: PathBuf, content: &str) {
19 self.analyze_file_internal(file_path, content, true);
20 }
21
22 pub(crate) fn analyze_file_fresh(&self, file_path: PathBuf, content: &str) {
25 self.analyze_file_internal(file_path, content, false);
26 }
27
28 fn analyze_file_internal(&self, file_path: PathBuf, content: &str, cleanup_previous: bool) {
30 let file_path = self.get_canonical_path(file_path);
32
33 debug!("Analyzing file: {:?}", file_path);
34
35 self.file_cache
38 .insert(file_path.clone(), std::sync::Arc::new(content.to_string()));
39
40 let parsed = match parse(content, Mode::Module, "") {
42 Ok(ast) => ast,
43 Err(e) => {
44 error!("Failed to parse Python file {:?}: {}", file_path, e);
45 return;
46 }
47 };
48
49 self.usages.remove(&file_path);
51
52 self.undeclared_fixtures.remove(&file_path);
54
55 self.imports.remove(&file_path);
57
58 if cleanup_previous {
65 self.cleanup_definitions_for_file(&file_path);
66 }
67
68 let is_conftest = file_path
70 .file_name()
71 .map(|n| n == "conftest.py")
72 .unwrap_or(false);
73 debug!("is_conftest: {}", is_conftest);
74
75 let line_index = self.get_line_index(&file_path, content);
77
78 if let rustpython_parser::ast::Mod::Module(module) = parsed {
80 debug!("Module has {} statements", module.body.len());
81
82 let mut module_level_names = HashSet::new();
84 for stmt in &module.body {
85 self.collect_module_level_names(stmt, &mut module_level_names);
86 }
87 self.imports.insert(file_path.clone(), module_level_names);
88
89 for stmt in &module.body {
91 self.visit_stmt(stmt, &file_path, is_conftest, content, &line_index);
92 }
93 }
94
95 debug!("Analysis complete for {:?}", file_path);
96 }
97
98 fn cleanup_definitions_for_file(&self, file_path: &PathBuf) {
100 let keys: Vec<String> = {
106 let mut k = Vec::new();
107 for entry in self.definitions.iter() {
108 k.push(entry.key().clone());
109 }
110 k
111 }; for key in keys {
115 let current_defs = match self.definitions.get(&key) {
117 Some(defs) => defs.clone(),
118 None => continue,
119 };
120
121 let filtered: Vec<FixtureDefinition> = current_defs
123 .iter()
124 .filter(|def| def.file_path != *file_path)
125 .cloned()
126 .collect();
127
128 if filtered.is_empty() {
130 self.definitions.remove(&key);
131 } else if filtered.len() != current_defs.len() {
132 self.definitions.insert(key, filtered);
134 }
135 }
136 }
137
138 pub(crate) fn build_line_index(content: &str) -> Vec<usize> {
140 let mut line_index = Vec::with_capacity(content.len() / 30);
141 line_index.push(0);
142 for (i, c) in content.char_indices() {
143 if c == '\n' {
144 line_index.push(i + 1);
145 }
146 }
147 line_index
148 }
149
150 pub(crate) fn get_line_from_offset(&self, offset: usize, line_index: &[usize]) -> usize {
152 match line_index.binary_search(&offset) {
153 Ok(line) => line + 1,
154 Err(line) => line,
155 }
156 }
157
158 pub(crate) fn get_char_position_from_offset(
160 &self,
161 offset: usize,
162 line_index: &[usize],
163 ) -> usize {
164 let line = self.get_line_from_offset(offset, line_index);
165 let line_start = line_index[line - 1];
166 offset.saturating_sub(line_start)
167 }
168
169 pub(crate) fn all_args(args: &Arguments) -> impl Iterator<Item = &ArgWithDefault> {
173 args.posonlyargs
174 .iter()
175 .chain(args.args.iter())
176 .chain(args.kwonlyargs.iter())
177 }
178
179 fn record_fixture_usage(
182 &self,
183 file_path: &Path,
184 fixture_name: String,
185 line: usize,
186 start_char: usize,
187 end_char: usize,
188 ) {
189 let file_path_buf = file_path.to_path_buf();
190 let usage = FixtureUsage {
191 name: fixture_name,
192 file_path: file_path_buf.clone(),
193 line,
194 start_char,
195 end_char,
196 };
197 self.usages.entry(file_path_buf).or_default().push(usage);
198 }
199
200 fn visit_stmt(
202 &self,
203 stmt: &Stmt,
204 file_path: &PathBuf,
205 _is_conftest: bool,
206 content: &str,
207 line_index: &[usize],
208 ) {
209 if let Stmt::Assign(assign) = stmt {
211 self.visit_assignment_fixture(assign, file_path, content, line_index);
212 }
213
214 if let Stmt::ClassDef(class_def) = stmt {
216 for decorator in &class_def.decorator_list {
218 let usefixtures = decorators::extract_usefixtures_names(decorator);
219 for (fixture_name, range) in usefixtures {
220 let usage_line =
221 self.get_line_from_offset(range.start().to_usize(), line_index);
222 let start_char =
223 self.get_char_position_from_offset(range.start().to_usize(), line_index);
224 let end_char =
225 self.get_char_position_from_offset(range.end().to_usize(), line_index);
226
227 info!(
228 "Found usefixtures usage on class: {} at {:?}:{}:{}",
229 fixture_name, file_path, usage_line, start_char
230 );
231
232 self.record_fixture_usage(
233 file_path,
234 fixture_name,
235 usage_line,
236 start_char + 1,
237 end_char - 1,
238 );
239 }
240 }
241
242 for class_stmt in &class_def.body {
243 self.visit_stmt(class_stmt, file_path, _is_conftest, content, line_index);
244 }
245 return;
246 }
247
248 let (func_name, decorator_list, args, range, body, returns) = match stmt {
250 Stmt::FunctionDef(func_def) => (
251 func_def.name.as_str(),
252 &func_def.decorator_list,
253 &func_def.args,
254 func_def.range,
255 &func_def.body,
256 &func_def.returns,
257 ),
258 Stmt::AsyncFunctionDef(func_def) => (
259 func_def.name.as_str(),
260 &func_def.decorator_list,
261 &func_def.args,
262 func_def.range,
263 &func_def.body,
264 &func_def.returns,
265 ),
266 _ => return,
267 };
268
269 debug!("Found function: {}", func_name);
270
271 for decorator in decorator_list {
273 let usefixtures = decorators::extract_usefixtures_names(decorator);
274 for (fixture_name, range) in usefixtures {
275 let usage_line = self.get_line_from_offset(range.start().to_usize(), line_index);
276 let start_char =
277 self.get_char_position_from_offset(range.start().to_usize(), line_index);
278 let end_char =
279 self.get_char_position_from_offset(range.end().to_usize(), line_index);
280
281 info!(
282 "Found usefixtures usage on function: {} at {:?}:{}:{}",
283 fixture_name, file_path, usage_line, start_char
284 );
285
286 self.record_fixture_usage(
287 file_path,
288 fixture_name,
289 usage_line,
290 start_char + 1,
291 end_char - 1,
292 );
293 }
294 }
295
296 for decorator in decorator_list {
298 let indirect_fixtures = decorators::extract_parametrize_indirect_fixtures(decorator);
299 for (fixture_name, range) in indirect_fixtures {
300 let usage_line = self.get_line_from_offset(range.start().to_usize(), line_index);
301 let start_char =
302 self.get_char_position_from_offset(range.start().to_usize(), line_index);
303 let end_char =
304 self.get_char_position_from_offset(range.end().to_usize(), line_index);
305
306 info!(
307 "Found parametrize indirect fixture usage: {} at {:?}:{}:{}",
308 fixture_name, file_path, usage_line, start_char
309 );
310
311 self.record_fixture_usage(
312 file_path,
313 fixture_name,
314 usage_line,
315 start_char + 1,
316 end_char - 1,
317 );
318 }
319 }
320
321 debug!(
323 "Function {} has {} decorators",
324 func_name,
325 decorator_list.len()
326 );
327 let fixture_decorator = decorator_list
328 .iter()
329 .find(|dec| decorators::is_fixture_decorator(dec));
330
331 if let Some(decorator) = fixture_decorator {
332 debug!(" Decorator matched as fixture!");
333
334 let fixture_name = decorators::extract_fixture_name_from_decorator(decorator)
336 .unwrap_or_else(|| func_name.to_string());
337
338 let line = self.get_line_from_offset(range.start().to_usize(), line_index);
339 let docstring = self.extract_docstring(body);
340 let return_type = self.extract_return_type(returns, body, content);
341
342 info!(
343 "Found fixture definition: {} (function: {}) at {:?}:{}",
344 fixture_name, func_name, file_path, line
345 );
346
347 let (start_char, end_char) = self.find_function_name_position(content, line, func_name);
348
349 let is_third_party = file_path.to_string_lossy().contains("site-packages");
350 let definition = FixtureDefinition {
351 name: fixture_name.clone(),
352 file_path: file_path.clone(),
353 line,
354 start_char,
355 end_char,
356 docstring,
357 return_type,
358 is_third_party,
359 };
360
361 self.definitions
362 .entry(fixture_name)
363 .or_default()
364 .push(definition);
365
366 let mut declared_params: HashSet<String> = HashSet::new();
368 declared_params.insert("self".to_string());
369 declared_params.insert("request".to_string());
370 declared_params.insert(func_name.to_string());
371
372 for arg in Self::all_args(args) {
373 let arg_name = arg.def.arg.as_str();
374 declared_params.insert(arg_name.to_string());
375
376 if arg_name != "self" && arg_name != "request" {
377 let arg_line =
378 self.get_line_from_offset(arg.def.range.start().to_usize(), line_index);
379 let start_char = self.get_char_position_from_offset(
380 arg.def.range.start().to_usize(),
381 line_index,
382 );
383 let end_char = self
384 .get_char_position_from_offset(arg.def.range.end().to_usize(), line_index);
385
386 info!(
387 "Found fixture dependency: {} at {:?}:{}:{}",
388 arg_name, file_path, arg_line, start_char
389 );
390
391 self.record_fixture_usage(
392 file_path,
393 arg_name.to_string(),
394 arg_line,
395 start_char,
396 end_char,
397 );
398 }
399 }
400
401 let function_line = self.get_line_from_offset(range.start().to_usize(), line_index);
402 self.scan_function_body_for_undeclared_fixtures(
403 body,
404 file_path,
405 content,
406 line_index,
407 &declared_params,
408 func_name,
409 function_line,
410 );
411 }
412
413 let is_test = func_name.starts_with("test_");
415
416 if is_test {
417 debug!("Found test function: {}", func_name);
418
419 let mut declared_params: HashSet<String> = HashSet::new();
420 declared_params.insert("self".to_string());
421 declared_params.insert("request".to_string());
422
423 for arg in Self::all_args(args) {
424 let arg_name = arg.def.arg.as_str();
425 declared_params.insert(arg_name.to_string());
426
427 if arg_name != "self" {
428 let arg_offset = arg.def.range.start().to_usize();
429 let arg_line = self.get_line_from_offset(arg_offset, line_index);
430 let start_char = self.get_char_position_from_offset(arg_offset, line_index);
431 let end_char = self
432 .get_char_position_from_offset(arg.def.range.end().to_usize(), line_index);
433
434 debug!(
435 "Parameter {} at offset {}, calculated line {}, char {}",
436 arg_name, arg_offset, arg_line, start_char
437 );
438 info!(
439 "Found fixture usage: {} at {:?}:{}:{}",
440 arg_name, file_path, arg_line, start_char
441 );
442
443 self.record_fixture_usage(
444 file_path,
445 arg_name.to_string(),
446 arg_line,
447 start_char,
448 end_char,
449 );
450 }
451 }
452
453 let function_line = self.get_line_from_offset(range.start().to_usize(), line_index);
454 self.scan_function_body_for_undeclared_fixtures(
455 body,
456 file_path,
457 content,
458 line_index,
459 &declared_params,
460 func_name,
461 function_line,
462 );
463 }
464 }
465
466 fn visit_assignment_fixture(
468 &self,
469 assign: &rustpython_parser::ast::StmtAssign,
470 file_path: &PathBuf,
471 _content: &str,
472 line_index: &[usize],
473 ) {
474 if let Expr::Call(outer_call) = &*assign.value {
475 if let Expr::Call(inner_call) = &*outer_call.func {
476 if decorators::is_fixture_decorator(&inner_call.func) {
477 for target in &assign.targets {
478 if let Expr::Name(name) = target {
479 let fixture_name = name.id.as_str();
480 let line = self
481 .get_line_from_offset(assign.range.start().to_usize(), line_index);
482
483 let start_char = self.get_char_position_from_offset(
484 name.range.start().to_usize(),
485 line_index,
486 );
487 let end_char = self.get_char_position_from_offset(
488 name.range.end().to_usize(),
489 line_index,
490 );
491
492 info!(
493 "Found fixture assignment: {} at {:?}:{}:{}-{}",
494 fixture_name, file_path, line, start_char, end_char
495 );
496
497 let is_third_party =
498 file_path.to_string_lossy().contains("site-packages");
499 let definition = FixtureDefinition {
500 name: fixture_name.to_string(),
501 file_path: file_path.clone(),
502 line,
503 start_char,
504 end_char,
505 docstring: None,
506 return_type: None,
507 is_third_party,
508 };
509
510 self.definitions
511 .entry(fixture_name.to_string())
512 .or_default()
513 .push(definition);
514 }
515 }
516 }
517 }
518 }
519 }
520}
521
522impl FixtureDatabase {
524 fn collect_module_level_names(&self, stmt: &Stmt, names: &mut HashSet<String>) {
528 match stmt {
529 Stmt::Import(import_stmt) => {
530 for alias in &import_stmt.names {
531 let name = alias.asname.as_ref().unwrap_or(&alias.name);
532 names.insert(name.to_string());
533 }
534 }
535 Stmt::ImportFrom(import_from) => {
536 for alias in &import_from.names {
537 let name = alias.asname.as_ref().unwrap_or(&alias.name);
538 names.insert(name.to_string());
539 }
540 }
541 Stmt::FunctionDef(func_def) => {
542 let is_fixture = func_def
543 .decorator_list
544 .iter()
545 .any(decorators::is_fixture_decorator);
546 if !is_fixture {
547 names.insert(func_def.name.to_string());
548 }
549 }
550 Stmt::AsyncFunctionDef(func_def) => {
551 let is_fixture = func_def
552 .decorator_list
553 .iter()
554 .any(decorators::is_fixture_decorator);
555 if !is_fixture {
556 names.insert(func_def.name.to_string());
557 }
558 }
559 Stmt::ClassDef(class_def) => {
560 names.insert(class_def.name.to_string());
561 }
562 Stmt::Assign(assign) => {
563 for target in &assign.targets {
564 self.collect_names_from_expr(target, names);
565 }
566 }
567 Stmt::AnnAssign(ann_assign) => {
568 self.collect_names_from_expr(&ann_assign.target, names);
569 }
570 _ => {}
571 }
572 }
573
574 #[allow(clippy::only_used_in_recursion)]
575 fn collect_names_from_expr(&self, expr: &Expr, names: &mut HashSet<String>) {
576 match expr {
577 Expr::Name(name) => {
578 names.insert(name.id.to_string());
579 }
580 Expr::Tuple(tuple) => {
581 for elt in &tuple.elts {
582 self.collect_names_from_expr(elt, names);
583 }
584 }
585 Expr::List(list) => {
586 for elt in &list.elts {
587 self.collect_names_from_expr(elt, names);
588 }
589 }
590 _ => {}
591 }
592 }
593
594 fn extract_docstring(&self, body: &[Stmt]) -> Option<String> {
597 if let Some(Stmt::Expr(expr_stmt)) = body.first() {
598 if let Expr::Constant(constant) = &*expr_stmt.value {
599 if let rustpython_parser::ast::Constant::Str(s) = &constant.value {
600 return Some(self.format_docstring(s.to_string()));
601 }
602 }
603 }
604 None
605 }
606
607 fn format_docstring(&self, docstring: String) -> String {
608 super::string_utils::format_docstring(docstring)
609 }
610
611 fn extract_return_type(
612 &self,
613 returns: &Option<Box<rustpython_parser::ast::Expr>>,
614 body: &[Stmt],
615 content: &str,
616 ) -> Option<String> {
617 if let Some(return_expr) = returns {
618 let has_yield = self.contains_yield(body);
619
620 if has_yield {
621 return self.extract_yielded_type(return_expr, content);
622 } else {
623 return Some(self.expr_to_string(return_expr, content));
624 }
625 }
626 None
627 }
628
629 #[allow(clippy::only_used_in_recursion)]
630 fn contains_yield(&self, body: &[Stmt]) -> bool {
631 for stmt in body {
632 match stmt {
633 Stmt::Expr(expr_stmt) => {
634 if let Expr::Yield(_) | Expr::YieldFrom(_) = &*expr_stmt.value {
635 return true;
636 }
637 }
638 Stmt::If(if_stmt) => {
639 if self.contains_yield(&if_stmt.body) || self.contains_yield(&if_stmt.orelse) {
640 return true;
641 }
642 }
643 Stmt::For(for_stmt) => {
644 if self.contains_yield(&for_stmt.body) || self.contains_yield(&for_stmt.orelse)
645 {
646 return true;
647 }
648 }
649 Stmt::While(while_stmt) => {
650 if self.contains_yield(&while_stmt.body)
651 || self.contains_yield(&while_stmt.orelse)
652 {
653 return true;
654 }
655 }
656 Stmt::With(with_stmt) => {
657 if self.contains_yield(&with_stmt.body) {
658 return true;
659 }
660 }
661 Stmt::Try(try_stmt) => {
662 if self.contains_yield(&try_stmt.body)
663 || self.contains_yield(&try_stmt.orelse)
664 || self.contains_yield(&try_stmt.finalbody)
665 {
666 return true;
667 }
668 }
669 _ => {}
670 }
671 }
672 false
673 }
674
675 fn extract_yielded_type(
676 &self,
677 expr: &rustpython_parser::ast::Expr,
678 content: &str,
679 ) -> Option<String> {
680 if let Expr::Subscript(subscript) = expr {
681 let _base_name = self.expr_to_string(&subscript.value, content);
682
683 if let Expr::Tuple(tuple) = &*subscript.slice {
684 if let Some(first_elem) = tuple.elts.first() {
685 return Some(self.expr_to_string(first_elem, content));
686 }
687 } else {
688 return Some(self.expr_to_string(&subscript.slice, content));
689 }
690 }
691
692 Some(self.expr_to_string(expr, content))
693 }
694
695 #[allow(clippy::only_used_in_recursion)]
696 fn expr_to_string(&self, expr: &rustpython_parser::ast::Expr, content: &str) -> String {
697 match expr {
698 Expr::Name(name) => name.id.to_string(),
699 Expr::Attribute(attr) => {
700 format!(
701 "{}.{}",
702 self.expr_to_string(&attr.value, content),
703 attr.attr
704 )
705 }
706 Expr::Subscript(subscript) => {
707 let base = self.expr_to_string(&subscript.value, content);
708 let slice = self.expr_to_string(&subscript.slice, content);
709 format!("{}[{}]", base, slice)
710 }
711 Expr::Tuple(tuple) => {
712 let elements: Vec<String> = tuple
713 .elts
714 .iter()
715 .map(|e| self.expr_to_string(e, content))
716 .collect();
717 elements.join(", ")
718 }
719 Expr::Constant(constant) => {
720 format!("{:?}", constant.value)
721 }
722 Expr::BinOp(binop) if matches!(binop.op, rustpython_parser::ast::Operator::BitOr) => {
723 format!(
724 "{} | {}",
725 self.expr_to_string(&binop.left, content),
726 self.expr_to_string(&binop.right, content)
727 )
728 }
729 _ => "Any".to_string(),
730 }
731 }
732
733 fn find_function_name_position(
735 &self,
736 content: &str,
737 line: usize,
738 func_name: &str,
739 ) -> (usize, usize) {
740 super::string_utils::find_function_name_position(content, line, func_name)
741 }
742}
743
744impl FixtureDatabase {
746 #[allow(clippy::too_many_arguments)]
747 fn scan_function_body_for_undeclared_fixtures(
748 &self,
749 body: &[Stmt],
750 file_path: &PathBuf,
751 content: &str,
752 line_index: &[usize],
753 declared_params: &HashSet<String>,
754 function_name: &str,
755 function_line: usize,
756 ) {
757 let mut local_vars = HashMap::new();
759 self.collect_local_variables(body, content, line_index, &mut local_vars);
760
761 if let Some(imports) = self.imports.get(file_path) {
763 for import in imports.iter() {
764 local_vars.insert(import.clone(), 0);
765 }
766 }
767
768 for stmt in body {
770 self.visit_stmt_for_names(
771 stmt,
772 file_path,
773 content,
774 line_index,
775 declared_params,
776 &local_vars,
777 function_name,
778 function_line,
779 );
780 }
781 }
782
783 #[allow(clippy::only_used_in_recursion)]
784 fn collect_local_variables(
785 &self,
786 body: &[Stmt],
787 content: &str,
788 line_index: &[usize],
789 local_vars: &mut HashMap<String, usize>,
790 ) {
791 for stmt in body {
792 match stmt {
793 Stmt::Assign(assign) => {
794 let line =
795 self.get_line_from_offset(assign.range.start().to_usize(), line_index);
796 let mut temp_names = HashSet::new();
797 for target in &assign.targets {
798 self.collect_names_from_expr(target, &mut temp_names);
799 }
800 for name in temp_names {
801 local_vars.insert(name, line);
802 }
803 }
804 Stmt::AnnAssign(ann_assign) => {
805 let line =
806 self.get_line_from_offset(ann_assign.range.start().to_usize(), line_index);
807 let mut temp_names = HashSet::new();
808 self.collect_names_from_expr(&ann_assign.target, &mut temp_names);
809 for name in temp_names {
810 local_vars.insert(name, line);
811 }
812 }
813 Stmt::AugAssign(aug_assign) => {
814 let line =
815 self.get_line_from_offset(aug_assign.range.start().to_usize(), line_index);
816 let mut temp_names = HashSet::new();
817 self.collect_names_from_expr(&aug_assign.target, &mut temp_names);
818 for name in temp_names {
819 local_vars.insert(name, line);
820 }
821 }
822 Stmt::For(for_stmt) => {
823 let line =
824 self.get_line_from_offset(for_stmt.range.start().to_usize(), line_index);
825 let mut temp_names = HashSet::new();
826 self.collect_names_from_expr(&for_stmt.target, &mut temp_names);
827 for name in temp_names {
828 local_vars.insert(name, line);
829 }
830 self.collect_local_variables(&for_stmt.body, content, line_index, local_vars);
831 }
832 Stmt::AsyncFor(for_stmt) => {
833 let line =
834 self.get_line_from_offset(for_stmt.range.start().to_usize(), line_index);
835 let mut temp_names = HashSet::new();
836 self.collect_names_from_expr(&for_stmt.target, &mut temp_names);
837 for name in temp_names {
838 local_vars.insert(name, line);
839 }
840 self.collect_local_variables(&for_stmt.body, content, line_index, local_vars);
841 }
842 Stmt::While(while_stmt) => {
843 self.collect_local_variables(&while_stmt.body, content, line_index, local_vars);
844 }
845 Stmt::If(if_stmt) => {
846 self.collect_local_variables(&if_stmt.body, content, line_index, local_vars);
847 self.collect_local_variables(&if_stmt.orelse, content, line_index, local_vars);
848 }
849 Stmt::With(with_stmt) => {
850 let line =
851 self.get_line_from_offset(with_stmt.range.start().to_usize(), line_index);
852 for item in &with_stmt.items {
853 if let Some(ref optional_vars) = item.optional_vars {
854 let mut temp_names = HashSet::new();
855 self.collect_names_from_expr(optional_vars, &mut temp_names);
856 for name in temp_names {
857 local_vars.insert(name, line);
858 }
859 }
860 }
861 self.collect_local_variables(&with_stmt.body, content, line_index, local_vars);
862 }
863 Stmt::AsyncWith(with_stmt) => {
864 let line =
865 self.get_line_from_offset(with_stmt.range.start().to_usize(), line_index);
866 for item in &with_stmt.items {
867 if let Some(ref optional_vars) = item.optional_vars {
868 let mut temp_names = HashSet::new();
869 self.collect_names_from_expr(optional_vars, &mut temp_names);
870 for name in temp_names {
871 local_vars.insert(name, line);
872 }
873 }
874 }
875 self.collect_local_variables(&with_stmt.body, content, line_index, local_vars);
876 }
877 Stmt::Try(try_stmt) => {
878 self.collect_local_variables(&try_stmt.body, content, line_index, local_vars);
879 self.collect_local_variables(&try_stmt.orelse, content, line_index, local_vars);
880 self.collect_local_variables(
881 &try_stmt.finalbody,
882 content,
883 line_index,
884 local_vars,
885 );
886 }
887 _ => {}
888 }
889 }
890 }
891
892 #[allow(clippy::too_many_arguments)]
893 fn visit_stmt_for_names(
894 &self,
895 stmt: &Stmt,
896 file_path: &PathBuf,
897 content: &str,
898 line_index: &[usize],
899 declared_params: &HashSet<String>,
900 local_vars: &HashMap<String, usize>,
901 function_name: &str,
902 function_line: usize,
903 ) {
904 match stmt {
905 Stmt::Expr(expr_stmt) => {
906 self.visit_expr_for_names(
907 &expr_stmt.value,
908 file_path,
909 content,
910 line_index,
911 declared_params,
912 local_vars,
913 function_name,
914 function_line,
915 );
916 }
917 Stmt::Assign(assign) => {
918 self.visit_expr_for_names(
919 &assign.value,
920 file_path,
921 content,
922 line_index,
923 declared_params,
924 local_vars,
925 function_name,
926 function_line,
927 );
928 }
929 Stmt::AugAssign(aug_assign) => {
930 self.visit_expr_for_names(
931 &aug_assign.value,
932 file_path,
933 content,
934 line_index,
935 declared_params,
936 local_vars,
937 function_name,
938 function_line,
939 );
940 }
941 Stmt::Return(ret) => {
942 if let Some(ref value) = ret.value {
943 self.visit_expr_for_names(
944 value,
945 file_path,
946 content,
947 line_index,
948 declared_params,
949 local_vars,
950 function_name,
951 function_line,
952 );
953 }
954 }
955 Stmt::If(if_stmt) => {
956 self.visit_expr_for_names(
957 &if_stmt.test,
958 file_path,
959 content,
960 line_index,
961 declared_params,
962 local_vars,
963 function_name,
964 function_line,
965 );
966 for stmt in &if_stmt.body {
967 self.visit_stmt_for_names(
968 stmt,
969 file_path,
970 content,
971 line_index,
972 declared_params,
973 local_vars,
974 function_name,
975 function_line,
976 );
977 }
978 for stmt in &if_stmt.orelse {
979 self.visit_stmt_for_names(
980 stmt,
981 file_path,
982 content,
983 line_index,
984 declared_params,
985 local_vars,
986 function_name,
987 function_line,
988 );
989 }
990 }
991 Stmt::While(while_stmt) => {
992 self.visit_expr_for_names(
993 &while_stmt.test,
994 file_path,
995 content,
996 line_index,
997 declared_params,
998 local_vars,
999 function_name,
1000 function_line,
1001 );
1002 for stmt in &while_stmt.body {
1003 self.visit_stmt_for_names(
1004 stmt,
1005 file_path,
1006 content,
1007 line_index,
1008 declared_params,
1009 local_vars,
1010 function_name,
1011 function_line,
1012 );
1013 }
1014 }
1015 Stmt::For(for_stmt) => {
1016 self.visit_expr_for_names(
1017 &for_stmt.iter,
1018 file_path,
1019 content,
1020 line_index,
1021 declared_params,
1022 local_vars,
1023 function_name,
1024 function_line,
1025 );
1026 for stmt in &for_stmt.body {
1027 self.visit_stmt_for_names(
1028 stmt,
1029 file_path,
1030 content,
1031 line_index,
1032 declared_params,
1033 local_vars,
1034 function_name,
1035 function_line,
1036 );
1037 }
1038 }
1039 Stmt::With(with_stmt) => {
1040 for item in &with_stmt.items {
1041 self.visit_expr_for_names(
1042 &item.context_expr,
1043 file_path,
1044 content,
1045 line_index,
1046 declared_params,
1047 local_vars,
1048 function_name,
1049 function_line,
1050 );
1051 }
1052 for stmt in &with_stmt.body {
1053 self.visit_stmt_for_names(
1054 stmt,
1055 file_path,
1056 content,
1057 line_index,
1058 declared_params,
1059 local_vars,
1060 function_name,
1061 function_line,
1062 );
1063 }
1064 }
1065 Stmt::AsyncFor(for_stmt) => {
1066 self.visit_expr_for_names(
1067 &for_stmt.iter,
1068 file_path,
1069 content,
1070 line_index,
1071 declared_params,
1072 local_vars,
1073 function_name,
1074 function_line,
1075 );
1076 for stmt in &for_stmt.body {
1077 self.visit_stmt_for_names(
1078 stmt,
1079 file_path,
1080 content,
1081 line_index,
1082 declared_params,
1083 local_vars,
1084 function_name,
1085 function_line,
1086 );
1087 }
1088 }
1089 Stmt::AsyncWith(with_stmt) => {
1090 for item in &with_stmt.items {
1091 self.visit_expr_for_names(
1092 &item.context_expr,
1093 file_path,
1094 content,
1095 line_index,
1096 declared_params,
1097 local_vars,
1098 function_name,
1099 function_line,
1100 );
1101 }
1102 for stmt in &with_stmt.body {
1103 self.visit_stmt_for_names(
1104 stmt,
1105 file_path,
1106 content,
1107 line_index,
1108 declared_params,
1109 local_vars,
1110 function_name,
1111 function_line,
1112 );
1113 }
1114 }
1115 Stmt::Assert(assert_stmt) => {
1116 self.visit_expr_for_names(
1117 &assert_stmt.test,
1118 file_path,
1119 content,
1120 line_index,
1121 declared_params,
1122 local_vars,
1123 function_name,
1124 function_line,
1125 );
1126 if let Some(ref msg) = assert_stmt.msg {
1127 self.visit_expr_for_names(
1128 msg,
1129 file_path,
1130 content,
1131 line_index,
1132 declared_params,
1133 local_vars,
1134 function_name,
1135 function_line,
1136 );
1137 }
1138 }
1139 _ => {}
1140 }
1141 }
1142
1143 #[allow(clippy::too_many_arguments, clippy::only_used_in_recursion)]
1144 fn visit_expr_for_names(
1145 &self,
1146 expr: &Expr,
1147 file_path: &PathBuf,
1148 content: &str,
1149 line_index: &[usize],
1150 declared_params: &HashSet<String>,
1151 local_vars: &HashMap<String, usize>,
1152 function_name: &str,
1153 function_line: usize,
1154 ) {
1155 match expr {
1156 Expr::Name(name) => {
1157 let name_str = name.id.as_str();
1158 let line = self.get_line_from_offset(name.range.start().to_usize(), line_index);
1159
1160 let is_local_var_in_scope = local_vars
1161 .get(name_str)
1162 .map(|def_line| *def_line < line)
1163 .unwrap_or(false);
1164
1165 if !declared_params.contains(name_str)
1166 && !is_local_var_in_scope
1167 && self.is_available_fixture(file_path, name_str)
1168 {
1169 let start_char = self
1170 .get_char_position_from_offset(name.range.start().to_usize(), line_index);
1171 let end_char =
1172 self.get_char_position_from_offset(name.range.end().to_usize(), line_index);
1173
1174 info!(
1175 "Found undeclared fixture usage: {} at {:?}:{}:{} in function {}",
1176 name_str, file_path, line, start_char, function_name
1177 );
1178
1179 let undeclared = UndeclaredFixture {
1180 name: name_str.to_string(),
1181 file_path: file_path.clone(),
1182 line,
1183 start_char,
1184 end_char,
1185 function_name: function_name.to_string(),
1186 function_line,
1187 };
1188
1189 self.undeclared_fixtures
1190 .entry(file_path.clone())
1191 .or_default()
1192 .push(undeclared);
1193 }
1194 }
1195 Expr::Call(call) => {
1196 self.visit_expr_for_names(
1197 &call.func,
1198 file_path,
1199 content,
1200 line_index,
1201 declared_params,
1202 local_vars,
1203 function_name,
1204 function_line,
1205 );
1206 for arg in &call.args {
1207 self.visit_expr_for_names(
1208 arg,
1209 file_path,
1210 content,
1211 line_index,
1212 declared_params,
1213 local_vars,
1214 function_name,
1215 function_line,
1216 );
1217 }
1218 }
1219 Expr::Attribute(attr) => {
1220 self.visit_expr_for_names(
1221 &attr.value,
1222 file_path,
1223 content,
1224 line_index,
1225 declared_params,
1226 local_vars,
1227 function_name,
1228 function_line,
1229 );
1230 }
1231 Expr::BinOp(binop) => {
1232 self.visit_expr_for_names(
1233 &binop.left,
1234 file_path,
1235 content,
1236 line_index,
1237 declared_params,
1238 local_vars,
1239 function_name,
1240 function_line,
1241 );
1242 self.visit_expr_for_names(
1243 &binop.right,
1244 file_path,
1245 content,
1246 line_index,
1247 declared_params,
1248 local_vars,
1249 function_name,
1250 function_line,
1251 );
1252 }
1253 Expr::UnaryOp(unaryop) => {
1254 self.visit_expr_for_names(
1255 &unaryop.operand,
1256 file_path,
1257 content,
1258 line_index,
1259 declared_params,
1260 local_vars,
1261 function_name,
1262 function_line,
1263 );
1264 }
1265 Expr::Compare(compare) => {
1266 self.visit_expr_for_names(
1267 &compare.left,
1268 file_path,
1269 content,
1270 line_index,
1271 declared_params,
1272 local_vars,
1273 function_name,
1274 function_line,
1275 );
1276 for comparator in &compare.comparators {
1277 self.visit_expr_for_names(
1278 comparator,
1279 file_path,
1280 content,
1281 line_index,
1282 declared_params,
1283 local_vars,
1284 function_name,
1285 function_line,
1286 );
1287 }
1288 }
1289 Expr::Subscript(subscript) => {
1290 self.visit_expr_for_names(
1291 &subscript.value,
1292 file_path,
1293 content,
1294 line_index,
1295 declared_params,
1296 local_vars,
1297 function_name,
1298 function_line,
1299 );
1300 self.visit_expr_for_names(
1301 &subscript.slice,
1302 file_path,
1303 content,
1304 line_index,
1305 declared_params,
1306 local_vars,
1307 function_name,
1308 function_line,
1309 );
1310 }
1311 Expr::List(list) => {
1312 for elt in &list.elts {
1313 self.visit_expr_for_names(
1314 elt,
1315 file_path,
1316 content,
1317 line_index,
1318 declared_params,
1319 local_vars,
1320 function_name,
1321 function_line,
1322 );
1323 }
1324 }
1325 Expr::Tuple(tuple) => {
1326 for elt in &tuple.elts {
1327 self.visit_expr_for_names(
1328 elt,
1329 file_path,
1330 content,
1331 line_index,
1332 declared_params,
1333 local_vars,
1334 function_name,
1335 function_line,
1336 );
1337 }
1338 }
1339 Expr::Dict(dict) => {
1340 for k in dict.keys.iter().flatten() {
1341 self.visit_expr_for_names(
1342 k,
1343 file_path,
1344 content,
1345 line_index,
1346 declared_params,
1347 local_vars,
1348 function_name,
1349 function_line,
1350 );
1351 }
1352 for value in &dict.values {
1353 self.visit_expr_for_names(
1354 value,
1355 file_path,
1356 content,
1357 line_index,
1358 declared_params,
1359 local_vars,
1360 function_name,
1361 function_line,
1362 );
1363 }
1364 }
1365 Expr::Await(await_expr) => {
1366 self.visit_expr_for_names(
1367 &await_expr.value,
1368 file_path,
1369 content,
1370 line_index,
1371 declared_params,
1372 local_vars,
1373 function_name,
1374 function_line,
1375 );
1376 }
1377 _ => {}
1378 }
1379 }
1380
1381 pub(crate) fn is_available_fixture(&self, file_path: &Path, fixture_name: &str) -> bool {
1383 if let Some(definitions) = self.definitions.get(fixture_name) {
1384 for def in definitions.iter() {
1385 if def.file_path == file_path {
1387 return true;
1388 }
1389
1390 if def.file_path.file_name().and_then(|n| n.to_str()) == Some("conftest.py")
1392 && file_path.starts_with(def.file_path.parent().unwrap_or(Path::new("")))
1393 {
1394 return true;
1395 }
1396
1397 if def.is_third_party {
1399 return true;
1400 }
1401 }
1402 }
1403 false
1404 }
1405}