1use super::decorators;
8use super::types::{FixtureDefinition, FixtureUsage, TypeImportSpec};
9use super::FixtureDatabase;
10use once_cell::sync::Lazy;
11use rustpython_parser::ast::{ArgWithDefault, Arguments, Expr, Stmt};
12use rustpython_parser::{parse, Mode};
13use std::collections::{HashMap, HashSet};
14use std::path::{Path, PathBuf};
15use tracing::{debug, info};
16
17impl FixtureDatabase {
18 pub fn analyze_file(&self, file_path: PathBuf, content: &str) {
21 self.analyze_file_internal(file_path, content, true);
22 }
23
24 pub(crate) fn analyze_file_fresh(&self, file_path: PathBuf, content: &str) {
27 self.analyze_file_internal(file_path, content, false);
28 }
29
30 fn analyze_file_internal(&self, file_path: PathBuf, content: &str, cleanup_previous: bool) {
32 let file_path = self.get_canonical_path(file_path);
34
35 debug!("Analyzing file: {:?}", file_path);
36
37 self.file_cache
40 .insert(file_path.clone(), std::sync::Arc::new(content.to_string()));
41
42 let parsed = match parse(content, Mode::Module, "") {
44 Ok(ast) => ast,
45 Err(e) => {
46 debug!(
49 "Failed to parse Python file {:?}: {} - keeping previous data",
50 file_path, e
51 );
52 return;
53 }
54 };
55
56 self.cleanup_usages_for_file(&file_path);
58 self.usages.remove(&file_path);
59
60 self.undeclared_fixtures.remove(&file_path);
62
63 self.imports.remove(&file_path);
65
66 if cleanup_previous {
73 self.cleanup_definitions_for_file(&file_path);
74 }
75
76 let is_conftest = file_path
78 .file_name()
79 .map(|n| n == "conftest.py")
80 .unwrap_or(false);
81 debug!("is_conftest: {}", is_conftest);
82
83 let line_index = self.get_line_index(&file_path, content);
85
86 if let rustpython_parser::ast::Mod::Module(module) = parsed {
88 debug!("Module has {} statements", module.body.len());
89
90 let mut module_level_names = HashSet::new();
92 for stmt in &module.body {
93 self.collect_module_level_names(stmt, &mut module_level_names);
94 }
95 self.imports
101 .insert(file_path.clone(), module_level_names.clone());
102
103 let import_map = self.build_name_to_import_map(&module.body, &file_path);
106
107 let type_aliases = self.collect_type_aliases(&module.body, content);
110
111 for stmt in &module.body {
113 self.visit_stmt(
114 stmt,
115 &file_path,
116 is_conftest,
117 content,
118 &line_index,
119 &import_map,
120 &module_level_names,
121 &type_aliases,
122 );
123 }
124 }
125
126 debug!("Analysis complete for {:?}", file_path);
127
128 self.evict_cache_if_needed();
130 }
131
132 fn cleanup_definitions_for_file(&self, file_path: &PathBuf) {
142 let fixture_names = match self.file_definitions.remove(file_path) {
144 Some((_, names)) => names,
145 None => return, };
147
148 for fixture_name in fixture_names {
150 let should_remove = {
151 if let Some(mut defs) = self.definitions.get_mut(&fixture_name) {
153 defs.retain(|def| def.file_path != *file_path);
154 defs.is_empty()
155 } else {
156 false
157 }
158 }; if should_remove {
162 self.definitions
165 .remove_if(&fixture_name, |_, defs| defs.is_empty());
166 }
167 }
168 }
169
170 fn cleanup_usages_for_file(&self, file_path: &PathBuf) {
176 let all_keys: Vec<String> = self
178 .usage_by_fixture
179 .iter()
180 .map(|entry| entry.key().clone())
181 .collect();
182
183 for fixture_name in all_keys {
185 let should_remove = {
186 if let Some(mut usages) = self.usage_by_fixture.get_mut(&fixture_name) {
187 let had_usages = usages.iter().any(|(path, _)| path == file_path);
188 if had_usages {
189 usages.retain(|(path, _)| path != file_path);
190 }
191 usages.is_empty()
192 } else {
193 false
194 }
195 };
196
197 if should_remove {
198 self.usage_by_fixture
199 .remove_if(&fixture_name, |_, usages| usages.is_empty());
200 }
201 }
202 }
203
204 pub(crate) fn build_line_index(content: &str) -> Vec<usize> {
207 let bytes = content.as_bytes();
208 let mut line_index = Vec::with_capacity(content.len() / 30);
209 line_index.push(0);
210 for i in memchr::memchr_iter(b'\n', bytes) {
211 line_index.push(i + 1);
212 }
213 line_index
214 }
215
216 pub(crate) fn get_line_from_offset(&self, offset: usize, line_index: &[usize]) -> usize {
218 match line_index.binary_search(&offset) {
219 Ok(line) => line + 1,
220 Err(line) => line,
221 }
222 }
223
224 pub(crate) fn get_char_position_from_offset(
226 &self,
227 offset: usize,
228 line_index: &[usize],
229 ) -> usize {
230 let line = self.get_line_from_offset(offset, line_index);
231 let line_start = line_index[line - 1];
232 offset.saturating_sub(line_start)
233 }
234
235 pub(crate) fn all_args(args: &Arguments) -> impl Iterator<Item = &ArgWithDefault> {
239 args.posonlyargs
240 .iter()
241 .chain(args.args.iter())
242 .chain(args.kwonlyargs.iter())
243 }
244
245 fn record_fixture_usage(
249 &self,
250 file_path: &Path,
251 fixture_name: String,
252 line: usize,
253 start_char: usize,
254 end_char: usize,
255 is_parameter: bool,
256 ) {
257 let file_path_buf = file_path.to_path_buf();
258 let usage = FixtureUsage {
259 name: fixture_name.clone(),
260 file_path: file_path_buf.clone(),
261 line,
262 start_char,
263 end_char,
264 is_parameter,
265 };
266
267 self.usages
269 .entry(file_path_buf.clone())
270 .or_default()
271 .push(usage.clone());
272
273 self.usage_by_fixture
275 .entry(fixture_name)
276 .or_default()
277 .push((file_path_buf, usage));
278 }
279
280 pub(crate) fn record_fixture_definition(&self, definition: FixtureDefinition) {
283 let file_path = definition.file_path.clone();
284 let fixture_name = definition.name.clone();
285
286 self.definitions
288 .entry(fixture_name.clone())
289 .or_default()
290 .push(definition);
291
292 self.file_definitions
294 .entry(file_path)
295 .or_default()
296 .insert(fixture_name);
297
298 self.invalidate_cycle_cache();
300 }
301
302 #[allow(clippy::too_many_arguments)]
304 fn visit_stmt(
305 &self,
306 stmt: &Stmt,
307 file_path: &PathBuf,
308 _is_conftest: bool,
309 content: &str,
310 line_index: &[usize],
311 import_map: &HashMap<String, TypeImportSpec>,
312 module_level_names: &HashSet<String>,
313 type_aliases: &HashMap<String, String>,
314 ) {
315 if let Stmt::Assign(assign) = stmt {
317 self.visit_assignment_fixture(assign, file_path, content, line_index);
318
319 let is_pytestmark = assign.targets.iter().any(
322 |target| matches!(target, Expr::Name(name) if name.id.as_str() == "pytestmark"),
323 );
324 if is_pytestmark {
325 self.visit_pytestmark_assignment(Some(&assign.value), file_path, line_index);
326 }
327 }
328
329 if let Stmt::AnnAssign(ann_assign) = stmt {
331 let is_pytestmark = matches!(
332 ann_assign.target.as_ref(),
333 Expr::Name(name) if name.id.as_str() == "pytestmark"
334 );
335 if is_pytestmark {
336 self.visit_pytestmark_assignment(
337 ann_assign.value.as_deref(),
338 file_path,
339 line_index,
340 );
341 }
342 }
343
344 if let Stmt::ClassDef(class_def) = stmt {
346 for decorator in &class_def.decorator_list {
348 let usefixtures = decorators::extract_usefixtures_names(decorator);
349 for (fixture_name, range) in usefixtures {
350 let usage_line =
351 self.get_line_from_offset(range.start().to_usize(), line_index);
352 let start_char =
353 self.get_char_position_from_offset(range.start().to_usize(), line_index);
354 let end_char =
355 self.get_char_position_from_offset(range.end().to_usize(), line_index);
356
357 info!(
358 "Found usefixtures usage on class: {} at {:?}:{}:{}",
359 fixture_name, file_path, usage_line, start_char
360 );
361
362 self.record_fixture_usage(
363 file_path,
364 fixture_name,
365 usage_line,
366 start_char + 1,
367 end_char - 1,
368 false, );
370 }
371 }
372
373 for class_stmt in &class_def.body {
374 self.visit_stmt(
375 class_stmt,
376 file_path,
377 _is_conftest,
378 content,
379 line_index,
380 import_map,
381 module_level_names,
382 type_aliases,
383 );
384 }
385 return;
386 }
387
388 let (func_name, decorator_list, args, range, body, returns) = match stmt {
390 Stmt::FunctionDef(func_def) => (
391 func_def.name.as_str(),
392 &func_def.decorator_list,
393 &func_def.args,
394 func_def.range,
395 &func_def.body,
396 &func_def.returns,
397 ),
398 Stmt::AsyncFunctionDef(func_def) => (
399 func_def.name.as_str(),
400 &func_def.decorator_list,
401 &func_def.args,
402 func_def.range,
403 &func_def.body,
404 &func_def.returns,
405 ),
406 _ => return,
407 };
408
409 debug!("Found function: {}", func_name);
410
411 for decorator in decorator_list {
413 let usefixtures = decorators::extract_usefixtures_names(decorator);
414 for (fixture_name, range) in usefixtures {
415 let usage_line = self.get_line_from_offset(range.start().to_usize(), line_index);
416 let start_char =
417 self.get_char_position_from_offset(range.start().to_usize(), line_index);
418 let end_char =
419 self.get_char_position_from_offset(range.end().to_usize(), line_index);
420
421 info!(
422 "Found usefixtures usage on function: {} at {:?}:{}:{}",
423 fixture_name, file_path, usage_line, start_char
424 );
425
426 self.record_fixture_usage(
427 file_path,
428 fixture_name,
429 usage_line,
430 start_char + 1,
431 end_char - 1,
432 false, );
434 }
435 }
436
437 for decorator in decorator_list {
439 let indirect_fixtures = decorators::extract_parametrize_indirect_fixtures(decorator);
440 for (fixture_name, range) in indirect_fixtures {
441 let usage_line = self.get_line_from_offset(range.start().to_usize(), line_index);
442 let start_char =
443 self.get_char_position_from_offset(range.start().to_usize(), line_index);
444 let end_char =
445 self.get_char_position_from_offset(range.end().to_usize(), line_index);
446
447 info!(
448 "Found parametrize indirect fixture usage: {} at {:?}:{}:{}",
449 fixture_name, file_path, usage_line, start_char
450 );
451
452 self.record_fixture_usage(
453 file_path,
454 fixture_name,
455 usage_line,
456 start_char + 1,
457 end_char - 1,
458 false, );
460 }
461 }
462
463 debug!(
465 "Function {} has {} decorators",
466 func_name,
467 decorator_list.len()
468 );
469 let fixture_decorator = decorator_list
470 .iter()
471 .find(|dec| decorators::is_fixture_decorator(dec));
472
473 if let Some(decorator) = fixture_decorator {
474 debug!(" Decorator matched as fixture!");
475
476 let fixture_name = decorators::extract_fixture_name_from_decorator(decorator)
478 .unwrap_or_else(|| func_name.to_string());
479
480 let scope = decorators::extract_fixture_scope(decorator).unwrap_or_default();
482 let autouse = decorators::extract_fixture_autouse(decorator);
483
484 let line = self.get_line_from_offset(range.start().to_usize(), line_index);
485 let docstring = self.extract_docstring(body);
486 let raw_return_type = self.extract_return_type(returns, body, content);
487 let return_type = raw_return_type.map(|rt| {
488 if type_aliases.is_empty() {
489 rt
490 } else {
491 let expanded = Self::expand_type_aliases(&rt, type_aliases);
492 if expanded != rt {
493 info!(
494 "Expanded type alias in fixture '{}': {} → {}",
495 fixture_name, rt, expanded
496 );
497 }
498 expanded
499 }
500 });
501 let return_type_imports = match &return_type {
502 Some(rt) => {
503 self.resolve_return_type_imports(rt, import_map, module_level_names, file_path)
504 }
505 None => vec![],
506 };
507
508 info!(
509 "Found fixture definition: {} (function: {}, scope: {:?}) at {:?}:{}",
510 fixture_name, func_name, scope, file_path, line
511 );
512
513 let (start_char, end_char) = self.find_function_name_position(content, line, func_name);
514
515 let is_third_party = file_path.to_string_lossy().contains("site-packages")
516 || self.is_editable_install_third_party(file_path);
517 let is_plugin = self.plugin_fixture_files.contains_key(file_path);
518
519 let mut declared_params: HashSet<String> = HashSet::new();
521 let mut dependencies: Vec<String> = Vec::new();
522 declared_params.insert("self".to_string());
523 declared_params.insert("request".to_string());
524 declared_params.insert(func_name.to_string());
525
526 for arg in Self::all_args(args) {
527 let arg_name = arg.def.arg.as_str();
528 declared_params.insert(arg_name.to_string());
529 if arg_name != "self" && arg_name != "request" {
531 dependencies.push(arg_name.to_string());
532 }
533 }
534
535 let end_line = self.get_line_from_offset(range.end().to_usize(), line_index);
537
538 let definition = FixtureDefinition {
539 name: fixture_name.clone(),
540 file_path: file_path.clone(),
541 line,
542 end_line,
543 start_char,
544 end_char,
545 docstring,
546 return_type,
547 return_type_imports,
548 is_third_party,
549 is_plugin,
550 dependencies: dependencies.clone(),
551 scope,
552 yield_line: self.find_yield_line(body, line_index),
553 autouse,
554 };
555
556 self.record_fixture_definition(definition);
557
558 for arg in Self::all_args(args) {
561 let arg_name = arg.def.arg.as_str();
562
563 if arg_name != "self" {
567 let arg_line =
568 self.get_line_from_offset(arg.def.range.start().to_usize(), line_index);
569 let start_char = self.get_char_position_from_offset(
570 arg.def.range.start().to_usize(),
571 line_index,
572 );
573 let end_char = start_char + arg_name.len();
575
576 info!(
577 "Found fixture parameter usage: {} at {:?}:{}:{}",
578 arg_name, file_path, arg_line, start_char
579 );
580
581 self.record_fixture_usage(
582 file_path,
583 arg_name.to_string(),
584 arg_line,
585 start_char,
586 end_char,
587 true, );
589 }
590 }
591
592 let function_line = self.get_line_from_offset(range.start().to_usize(), line_index);
593 self.scan_function_body_for_undeclared_fixtures(
594 body,
595 file_path,
596 line_index,
597 &declared_params,
598 func_name,
599 function_line,
600 );
601 }
602
603 let is_test = func_name.starts_with("test_");
605
606 if is_test {
607 debug!("Found test function: {}", func_name);
608
609 let mut declared_params: HashSet<String> = HashSet::new();
610 declared_params.insert("self".to_string());
611 declared_params.insert("request".to_string());
612
613 for arg in Self::all_args(args) {
614 let arg_name = arg.def.arg.as_str();
615 declared_params.insert(arg_name.to_string());
616
617 if arg_name != "self" {
618 let arg_offset = arg.def.range.start().to_usize();
619 let arg_line = self.get_line_from_offset(arg_offset, line_index);
620 let start_char = self.get_char_position_from_offset(arg_offset, line_index);
621 let end_char = start_char + arg_name.len();
623
624 debug!(
625 "Parameter {} at offset {}, calculated line {}, char {}",
626 arg_name, arg_offset, arg_line, start_char
627 );
628 info!(
629 "Found fixture usage: {} at {:?}:{}:{}",
630 arg_name, file_path, arg_line, start_char
631 );
632
633 self.record_fixture_usage(
634 file_path,
635 arg_name.to_string(),
636 arg_line,
637 start_char,
638 end_char,
639 true, );
641 }
642 }
643
644 let function_line = self.get_line_from_offset(range.start().to_usize(), line_index);
645 self.scan_function_body_for_undeclared_fixtures(
646 body,
647 file_path,
648 line_index,
649 &declared_params,
650 func_name,
651 function_line,
652 );
653 }
654 }
655
656 fn visit_assignment_fixture(
658 &self,
659 assign: &rustpython_parser::ast::StmtAssign,
660 file_path: &PathBuf,
661 _content: &str,
662 line_index: &[usize],
663 ) {
664 if let Expr::Call(outer_call) = &*assign.value {
665 if let Expr::Call(inner_call) = &*outer_call.func {
666 if decorators::is_fixture_decorator(&inner_call.func) {
667 for target in &assign.targets {
668 if let Expr::Name(name) = target {
669 let fixture_name = name.id.as_str();
670 let line = self
671 .get_line_from_offset(assign.range.start().to_usize(), line_index);
672
673 let start_char = self.get_char_position_from_offset(
674 name.range.start().to_usize(),
675 line_index,
676 );
677 let end_char = self.get_char_position_from_offset(
678 name.range.end().to_usize(),
679 line_index,
680 );
681
682 info!(
683 "Found fixture assignment: {} at {:?}:{}:{}-{}",
684 fixture_name, file_path, line, start_char, end_char
685 );
686
687 let is_third_party =
688 file_path.to_string_lossy().contains("site-packages")
689 || self.is_editable_install_third_party(file_path);
690 let is_plugin = self.plugin_fixture_files.contains_key(file_path);
691 let definition = FixtureDefinition {
692 name: fixture_name.to_string(),
693 file_path: file_path.clone(),
694 line,
695 end_line: line, start_char,
697 end_char,
698 docstring: None,
699 return_type: None,
700 return_type_imports: vec![],
701 is_third_party,
702 is_plugin,
703 dependencies: Vec::new(), scope: decorators::extract_fixture_scope(&outer_call.func)
705 .unwrap_or_default(),
706 yield_line: None, autouse: false, };
709
710 self.record_fixture_definition(definition);
711 }
712 }
713 }
714 }
715 }
716 }
717
718 fn visit_pytestmark_assignment(
726 &self,
727 value: Option<&Expr>,
728 file_path: &PathBuf,
729 line_index: &[usize],
730 ) {
731 let Some(value) = value else {
732 return;
733 };
734
735 let usefixtures = decorators::extract_usefixtures_from_expr(value);
736 for (fixture_name, range) in usefixtures {
737 let usage_line = self.get_line_from_offset(range.start().to_usize(), line_index);
738 let start_char =
739 self.get_char_position_from_offset(range.start().to_usize(), line_index);
740 let end_char = self.get_char_position_from_offset(range.end().to_usize(), line_index);
741
742 info!(
743 "Found usefixtures usage via pytestmark assignment: {} at {:?}:{}:{}",
744 fixture_name, file_path, usage_line, start_char
745 );
746
747 self.record_fixture_usage(
748 file_path,
749 fixture_name,
750 usage_line,
751 start_char.saturating_add(1),
752 end_char.saturating_sub(1),
753 false, );
755 }
756 }
757}
758
759static BUILTINS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
762 [
763 "int",
764 "str",
765 "bool",
766 "float",
767 "bytes",
768 "bytearray",
769 "complex",
770 "list",
771 "dict",
772 "tuple",
773 "set",
774 "frozenset",
775 "type",
776 "object",
777 "None",
778 "range",
779 "slice",
780 "memoryview",
781 "property",
782 "classmethod",
783 "staticmethod",
784 "super",
785 "Exception",
786 "BaseException",
787 "ValueError",
788 "TypeError",
789 "RuntimeError",
790 "NotImplementedError",
791 "AttributeError",
792 "KeyError",
793 "IndexError",
794 "StopIteration",
795 "GeneratorExit",
796 ]
797 .into_iter()
798 .collect()
799});
800
801impl FixtureDatabase {
803 pub(crate) fn collect_type_aliases(
817 &self,
818 stmts: &[Stmt],
819 content: &str,
820 ) -> HashMap<String, String> {
821 let mut aliases = HashMap::new();
822
823 for stmt in stmts {
824 match stmt {
825 Stmt::AnnAssign(ann_assign) => {
827 if !Self::annotation_is_type_alias(&ann_assign.annotation) {
828 continue;
829 }
830 let Expr::Name(name) = ann_assign.target.as_ref() else {
831 continue;
832 };
833 let Some(value) = &ann_assign.value else {
834 continue;
835 };
836 let expanded = self.expr_to_string(value, content);
837 if expanded != "Any" {
844 debug!("Type alias (PEP 613): {} = {}", name.id, expanded);
845 aliases.insert(name.id.to_string(), expanded);
846 }
847 }
848
849 Stmt::Assign(assign) => {
851 if assign.targets.len() != 1 {
852 continue;
853 }
854 let Expr::Name(name) = &assign.targets[0] else {
855 continue;
856 };
857 if !name.id.starts_with(|c: char| c.is_ascii_uppercase()) {
859 continue;
860 }
861 if !Self::expr_looks_like_type(&assign.value) {
862 continue;
863 }
864 let expanded = self.expr_to_string(&assign.value, content);
865 if expanded != "Any" {
868 debug!("Type alias (old-style): {} = {}", name.id, expanded);
869 aliases.insert(name.id.to_string(), expanded);
870 }
871 }
872
873 _ => {}
874 }
875 }
876
877 aliases
878 }
879
880 fn annotation_is_type_alias(expr: &Expr) -> bool {
884 match expr {
885 Expr::Name(name) => name.id.as_str() == "TypeAlias",
886 Expr::Attribute(attr) => {
887 attr.attr.as_str() == "TypeAlias"
888 && matches!(
889 attr.value.as_ref(),
890 Expr::Name(n) if n.id.as_str() == "typing" || n.id.as_str() == "typing_extensions"
891 )
892 }
893 _ => false,
894 }
895 }
896
897 fn expr_looks_like_type(expr: &Expr) -> bool {
903 match expr {
904 Expr::Subscript(_) => true,
906 Expr::BinOp(binop) => {
908 matches!(binop.op, rustpython_parser::ast::Operator::BitOr)
909 && Self::expr_looks_like_type(&binop.left)
910 && Self::expr_looks_like_type(&binop.right)
911 }
912 Expr::Name(name) => {
914 name.id.starts_with(|c: char| c.is_ascii_uppercase())
915 || BUILTINS.contains(name.id.as_str())
916 }
917 Expr::Attribute(_) => true,
919 Expr::Constant(c) => matches!(
921 c.value,
922 rustpython_parser::ast::Constant::None | rustpython_parser::ast::Constant::Str(_)
923 ),
924 _ => false,
925 }
926 }
927
928 pub(crate) fn expand_type_aliases(
939 type_str: &str,
940 type_aliases: &HashMap<String, String>,
941 ) -> String {
942 const MAX_DEPTH: usize = 5;
943 let mut result = type_str.to_string();
944
945 for _ in 0..MAX_DEPTH {
946 let mut changed = false;
947 for (alias, expanded) in type_aliases {
948 let new = super::string_utils::replace_identifier(&result, alias, expanded);
949 if new != result {
950 result = new;
951 changed = true;
952 }
953 }
954 if !changed {
955 break;
956 }
957 }
958
959 result
960 }
961
962 fn extract_type_identifiers(type_str: &str) -> Vec<&str> {
980 let mut identifiers = Vec::new();
981 let mut seen = HashSet::new();
982 let bytes = type_str.as_bytes();
983 let len = bytes.len();
984 let mut i = 0;
985
986 while i < len {
987 let b = bytes[i];
988 if b.is_ascii_alphabetic() || b == b'_' {
990 let start = i;
991 i += 1;
992 while i < len && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
993 i += 1;
994 }
995 let ident = &type_str[start..i];
996 if seen.insert(ident) {
997 identifiers.push(ident);
998 }
999 } else {
1000 i += 1;
1001 }
1002 }
1003
1004 identifiers
1005 }
1006
1007 fn resolve_return_type_imports(
1025 &self,
1026 return_type: &str,
1027 import_map: &HashMap<String, TypeImportSpec>,
1028 module_level_names: &HashSet<String>,
1029 fixture_file: &Path,
1030 ) -> Vec<TypeImportSpec> {
1031 let identifiers = Self::extract_type_identifiers(return_type);
1032 let mut specs: Vec<TypeImportSpec> = Vec::new();
1033 let mut seen: HashSet<&str> = HashSet::new();
1034
1035 for ident in identifiers {
1036 if BUILTINS.contains(ident) {
1038 continue;
1039 }
1040
1041 if !seen.insert(ident) {
1043 continue;
1044 }
1045
1046 if let Some(spec) = import_map.get(ident) {
1048 specs.push(spec.clone());
1049 continue;
1050 }
1051
1052 if module_level_names.contains(ident) {
1055 if let Some(module_path) = Self::file_path_to_module_path(fixture_file) {
1056 specs.push(TypeImportSpec {
1057 check_name: ident.to_string(),
1058 import_statement: format!("from {} import {}", module_path, ident),
1059 });
1060 }
1061 }
1062 }
1063
1064 specs
1065 }
1066
1067 fn collect_module_level_names(&self, stmt: &Stmt, names: &mut HashSet<String>) {
1071 match stmt {
1072 Stmt::Import(import_stmt) => {
1073 for alias in &import_stmt.names {
1074 let name = alias.asname.as_ref().unwrap_or(&alias.name);
1075 names.insert(name.to_string());
1076 }
1077 }
1078 Stmt::ImportFrom(import_from) => {
1079 for alias in &import_from.names {
1080 let name = alias.asname.as_ref().unwrap_or(&alias.name);
1081 names.insert(name.to_string());
1082 }
1083 }
1084 Stmt::FunctionDef(func_def) => {
1085 let is_fixture = func_def
1086 .decorator_list
1087 .iter()
1088 .any(decorators::is_fixture_decorator);
1089 if !is_fixture {
1090 names.insert(func_def.name.to_string());
1091 }
1092 }
1093 Stmt::AsyncFunctionDef(func_def) => {
1094 let is_fixture = func_def
1095 .decorator_list
1096 .iter()
1097 .any(decorators::is_fixture_decorator);
1098 if !is_fixture {
1099 names.insert(func_def.name.to_string());
1100 }
1101 }
1102 Stmt::ClassDef(class_def) => {
1103 names.insert(class_def.name.to_string());
1104 }
1105 Stmt::Assign(assign) => {
1106 for target in &assign.targets {
1107 self.collect_names_from_expr(target, names);
1108 }
1109 }
1110 Stmt::AnnAssign(ann_assign) => {
1111 self.collect_names_from_expr(&ann_assign.target, names);
1112 }
1113 _ => {}
1114 }
1115 }
1116
1117 #[allow(clippy::only_used_in_recursion)]
1118 pub(crate) fn collect_names_from_expr(&self, expr: &Expr, names: &mut HashSet<String>) {
1119 match expr {
1120 Expr::Name(name) => {
1121 names.insert(name.id.to_string());
1122 }
1123 Expr::Tuple(tuple) => {
1124 for elt in &tuple.elts {
1125 self.collect_names_from_expr(elt, names);
1126 }
1127 }
1128 Expr::List(list) => {
1129 for elt in &list.elts {
1130 self.collect_names_from_expr(elt, names);
1131 }
1132 }
1133 _ => {}
1134 }
1135 }
1136
1137 fn find_function_name_position(
1141 &self,
1142 content: &str,
1143 line: usize,
1144 func_name: &str,
1145 ) -> (usize, usize) {
1146 super::string_utils::find_function_name_position(content, line, func_name)
1147 }
1148
1149 fn find_yield_line(&self, body: &[Stmt], line_index: &[usize]) -> Option<usize> {
1152 for stmt in body {
1153 if let Some(line) = self.find_yield_in_stmt(stmt, line_index) {
1154 return Some(line);
1155 }
1156 }
1157 None
1158 }
1159
1160 fn find_yield_in_stmt(&self, stmt: &Stmt, line_index: &[usize]) -> Option<usize> {
1162 match stmt {
1163 Stmt::Expr(expr_stmt) => self.find_yield_in_expr(&expr_stmt.value, line_index),
1164 Stmt::If(if_stmt) => {
1165 for s in &if_stmt.body {
1167 if let Some(line) = self.find_yield_in_stmt(s, line_index) {
1168 return Some(line);
1169 }
1170 }
1171 for s in &if_stmt.orelse {
1173 if let Some(line) = self.find_yield_in_stmt(s, line_index) {
1174 return Some(line);
1175 }
1176 }
1177 None
1178 }
1179 Stmt::With(with_stmt) => {
1180 for s in &with_stmt.body {
1181 if let Some(line) = self.find_yield_in_stmt(s, line_index) {
1182 return Some(line);
1183 }
1184 }
1185 None
1186 }
1187 Stmt::AsyncWith(with_stmt) => {
1188 for s in &with_stmt.body {
1189 if let Some(line) = self.find_yield_in_stmt(s, line_index) {
1190 return Some(line);
1191 }
1192 }
1193 None
1194 }
1195 Stmt::Try(try_stmt) => {
1196 for s in &try_stmt.body {
1197 if let Some(line) = self.find_yield_in_stmt(s, line_index) {
1198 return Some(line);
1199 }
1200 }
1201 for handler in &try_stmt.handlers {
1202 let rustpython_parser::ast::ExceptHandler::ExceptHandler(h) = handler;
1203 for s in &h.body {
1204 if let Some(line) = self.find_yield_in_stmt(s, line_index) {
1205 return Some(line);
1206 }
1207 }
1208 }
1209 for s in &try_stmt.orelse {
1210 if let Some(line) = self.find_yield_in_stmt(s, line_index) {
1211 return Some(line);
1212 }
1213 }
1214 for s in &try_stmt.finalbody {
1215 if let Some(line) = self.find_yield_in_stmt(s, line_index) {
1216 return Some(line);
1217 }
1218 }
1219 None
1220 }
1221 Stmt::For(for_stmt) => {
1222 for s in &for_stmt.body {
1223 if let Some(line) = self.find_yield_in_stmt(s, line_index) {
1224 return Some(line);
1225 }
1226 }
1227 for s in &for_stmt.orelse {
1228 if let Some(line) = self.find_yield_in_stmt(s, line_index) {
1229 return Some(line);
1230 }
1231 }
1232 None
1233 }
1234 Stmt::AsyncFor(for_stmt) => {
1235 for s in &for_stmt.body {
1236 if let Some(line) = self.find_yield_in_stmt(s, line_index) {
1237 return Some(line);
1238 }
1239 }
1240 for s in &for_stmt.orelse {
1241 if let Some(line) = self.find_yield_in_stmt(s, line_index) {
1242 return Some(line);
1243 }
1244 }
1245 None
1246 }
1247 Stmt::While(while_stmt) => {
1248 for s in &while_stmt.body {
1249 if let Some(line) = self.find_yield_in_stmt(s, line_index) {
1250 return Some(line);
1251 }
1252 }
1253 for s in &while_stmt.orelse {
1254 if let Some(line) = self.find_yield_in_stmt(s, line_index) {
1255 return Some(line);
1256 }
1257 }
1258 None
1259 }
1260 _ => None,
1261 }
1262 }
1263
1264 fn find_yield_in_expr(&self, expr: &Expr, line_index: &[usize]) -> Option<usize> {
1266 match expr {
1267 Expr::Yield(yield_expr) => {
1268 let line =
1269 self.get_line_from_offset(yield_expr.range.start().to_usize(), line_index);
1270 Some(line)
1271 }
1272 Expr::YieldFrom(yield_from) => {
1273 let line =
1274 self.get_line_from_offset(yield_from.range.start().to_usize(), line_index);
1275 Some(line)
1276 }
1277 _ => None,
1278 }
1279 }
1280}
1281
1282