1use super::decorators;
7use super::types::{
8 CompletionContext, FixtureDefinition, FixtureUsage, ParamInsertionInfo, UndeclaredFixture,
9};
10use super::FixtureDatabase;
11use rustpython_parser::ast::{Expr, Ranged, Stmt};
12use std::collections::HashSet;
13use std::path::Path;
14use tracing::{debug, info};
15
16impl FixtureDatabase {
17 pub fn find_fixture_definition(
19 &self,
20 file_path: &Path,
21 line: u32,
22 character: u32,
23 ) -> Option<FixtureDefinition> {
24 debug!(
25 "find_fixture_definition: file={:?}, line={}, char={}",
26 file_path, line, character
27 );
28
29 let target_line = (line + 1) as usize; let content = self.get_file_content(file_path)?;
32 let line_content = content.lines().nth(target_line.saturating_sub(1))?;
33 debug!("Line content: {}", line_content);
34
35 let word_at_cursor = self.extract_word_at_position(line_content, character as usize)?;
36 debug!("Word at cursor: {:?}", word_at_cursor);
37
38 let current_fixture_def = self.get_fixture_definition_at_line(file_path, target_line);
40
41 if let Some(usages) = self.usages.get(file_path) {
43 for usage in usages.iter() {
44 if usage.line == target_line && usage.name == word_at_cursor {
45 let cursor_pos = character as usize;
46 if cursor_pos >= usage.start_char && cursor_pos < usage.end_char {
47 debug!(
48 "Cursor at {} is within usage range {}-{}: {}",
49 cursor_pos, usage.start_char, usage.end_char, usage.name
50 );
51 info!("Found fixture usage at cursor position: {}", usage.name);
52
53 if let Some(ref current_def) = current_fixture_def {
55 if current_def.name == word_at_cursor {
56 info!(
57 "Self-referencing fixture detected, finding parent definition"
58 );
59 return self.find_closest_definition_excluding(
60 file_path,
61 &usage.name,
62 Some(current_def),
63 );
64 }
65 }
66
67 return self.find_closest_definition(file_path, &usage.name);
68 }
69 }
70 }
71 }
72
73 debug!("Word at cursor '{}' is not a fixture usage", word_at_cursor);
74 None
75 }
76
77 fn get_fixture_definition_at_line(
79 &self,
80 file_path: &Path,
81 line: usize,
82 ) -> Option<FixtureDefinition> {
83 for entry in self.definitions.iter() {
84 for def in entry.value().iter() {
85 if def.file_path == file_path && def.line == line {
86 return Some(def.clone());
87 }
88 }
89 }
90 None
91 }
92
93 pub fn find_fixture_or_definition_at_position(
98 &self,
99 file_path: &Path,
100 line: u32,
101 character: u32,
102 ) -> Option<FixtureDefinition> {
103 if let Some(def) = self.find_fixture_definition(file_path, line, character) {
105 return Some(def);
106 }
107
108 let target_line = (line + 1) as usize; let content = self.get_file_content(file_path)?;
111 let line_content = content.lines().nth(target_line.saturating_sub(1))?;
112 let word_at_cursor = self.extract_word_at_position(line_content, character as usize)?;
113
114 if let Some(definitions) = self.definitions.get(&word_at_cursor) {
116 for def in definitions.iter() {
117 if def.file_path == file_path && def.line == target_line {
118 if character as usize >= def.start_char && (character as usize) < def.end_char {
120 return Some(def.clone());
121 }
122 }
123 }
124 }
125
126 None
127 }
128
129 pub fn get_definition_at_line(
131 &self,
132 file_path: &Path,
133 line: usize,
134 fixture_name: &str,
135 ) -> Option<FixtureDefinition> {
136 if let Some(definitions) = self.definitions.get(fixture_name) {
137 for def in definitions.iter() {
138 if def.file_path == file_path && def.line == line {
139 return Some(def.clone());
140 }
141 }
142 }
143 None
144 }
145
146 pub(crate) fn find_closest_definition(
148 &self,
149 file_path: &Path,
150 fixture_name: &str,
151 ) -> Option<FixtureDefinition> {
152 self.find_closest_definition_with_filter(file_path, fixture_name, |_| true)
153 }
154
155 pub(crate) fn find_closest_definition_excluding(
157 &self,
158 file_path: &Path,
159 fixture_name: &str,
160 exclude: Option<&FixtureDefinition>,
161 ) -> Option<FixtureDefinition> {
162 self.find_closest_definition_with_filter(file_path, fixture_name, |def| {
163 if let Some(excluded) = exclude {
164 def != excluded
165 } else {
166 true
167 }
168 })
169 }
170
171 fn find_closest_definition_with_filter<F>(
177 &self,
178 file_path: &Path,
179 fixture_name: &str,
180 filter: F,
181 ) -> Option<FixtureDefinition>
182 where
183 F: Fn(&FixtureDefinition) -> bool,
184 {
185 let definitions = self.definitions.get(fixture_name)?;
186
187 debug!(
189 "Checking for fixture {} in same file: {:?}",
190 fixture_name, file_path
191 );
192
193 if let Some(last_def) = definitions
194 .iter()
195 .filter(|def| def.file_path == file_path && filter(def))
196 .max_by_key(|def| def.line)
197 {
198 info!(
199 "Found fixture {} in same file at line {}",
200 fixture_name, last_def.line
201 );
202 return Some(last_def.clone());
203 }
204
205 let mut current_dir = file_path.parent()?;
207
208 debug!(
209 "Searching for fixture {} in conftest.py files starting from {:?}",
210 fixture_name, current_dir
211 );
212 loop {
213 let conftest_path = current_dir.join("conftest.py");
214 debug!(" Checking conftest.py at: {:?}", conftest_path);
215
216 for def in definitions.iter() {
218 if def.file_path == conftest_path && filter(def) {
219 info!(
220 "Found fixture {} in conftest.py: {:?}",
221 fixture_name, conftest_path
222 );
223 return Some(def.clone());
224 }
225 }
226
227 if conftest_path.exists()
229 && self.is_fixture_imported_in_file(fixture_name, &conftest_path)
230 {
231 debug!(
234 "Fixture {} is imported in conftest.py: {:?}",
235 fixture_name, conftest_path
236 );
237 if let Some(def) = definitions.iter().find(|def| filter(def)) {
239 info!(
240 "Found imported fixture {} via conftest.py: {:?} (original: {:?})",
241 fixture_name, conftest_path, def.file_path
242 );
243 return Some(def.clone());
244 }
245 }
246
247 match current_dir.parent() {
248 Some(parent) => current_dir = parent,
249 None => break,
250 }
251 }
252
253 debug!(
255 "No fixture {} found in conftest hierarchy, checking third-party",
256 fixture_name
257 );
258 for def in definitions.iter() {
259 if def.is_third_party && filter(def) {
260 info!(
261 "Found third-party fixture {} in site-packages: {:?}",
262 fixture_name, def.file_path
263 );
264 return Some(def.clone());
265 }
266 }
267
268 debug!(
269 "No fixture {} found in scope for {:?}",
270 fixture_name, file_path
271 );
272 None
273 }
274
275 pub fn find_fixture_at_position(
277 &self,
278 file_path: &Path,
279 line: u32,
280 character: u32,
281 ) -> Option<String> {
282 let target_line = (line + 1) as usize;
283
284 debug!(
285 "find_fixture_at_position: file={:?}, line={}, char={}",
286 file_path, target_line, character
287 );
288
289 let content = self.get_file_content(file_path)?;
290 let line_content = content.lines().nth(target_line.saturating_sub(1))?;
291 debug!("Line content: {}", line_content);
292
293 let word_at_cursor = self.extract_word_at_position(line_content, character as usize);
294 debug!("Word at cursor: {:?}", word_at_cursor);
295
296 if let Some(usages) = self.usages.get(file_path) {
298 for usage in usages.iter() {
299 if usage.line == target_line {
300 let cursor_pos = character as usize;
301 if cursor_pos >= usage.start_char && cursor_pos < usage.end_char {
302 debug!(
303 "Cursor at {} is within usage range {}-{}: {}",
304 cursor_pos, usage.start_char, usage.end_char, usage.name
305 );
306 info!("Found fixture usage at cursor position: {}", usage.name);
307 return Some(usage.name.clone());
308 }
309 }
310 }
311 }
312
313 for entry in self.definitions.iter() {
315 for def in entry.value().iter() {
316 if def.file_path == file_path && def.line == target_line {
317 if let Some(ref word) = word_at_cursor {
318 if word == &def.name {
319 info!(
320 "Found fixture definition name at cursor position: {}",
321 def.name
322 );
323 return Some(def.name.clone());
324 }
325 }
326 }
327 }
328 }
329
330 debug!("No fixture found at cursor position");
331 None
332 }
333
334 pub fn extract_word_at_position(&self, line: &str, character: usize) -> Option<String> {
336 super::string_utils::extract_word_at_position(line, character)
337 }
338
339 pub fn find_fixture_references(&self, fixture_name: &str) -> Vec<FixtureUsage> {
341 info!("Finding all references for fixture: {}", fixture_name);
342
343 let mut all_references = Vec::new();
344
345 for entry in self.usages.iter() {
346 let file_path = entry.key();
347 let usages = entry.value();
348
349 for usage in usages.iter() {
350 if usage.name == fixture_name {
351 debug!(
352 "Found reference to {} in {:?} at line {}",
353 fixture_name, file_path, usage.line
354 );
355 all_references.push(usage.clone());
356 }
357 }
358 }
359
360 info!(
361 "Found {} total references for fixture: {}",
362 all_references.len(),
363 fixture_name
364 );
365 all_references
366 }
367
368 pub fn find_references_for_definition(
372 &self,
373 definition: &FixtureDefinition,
374 ) -> Vec<FixtureUsage> {
375 info!(
376 "Finding references for specific definition: {} at {:?}:{}",
377 definition.name, definition.file_path, definition.line
378 );
379
380 let mut matching_references = Vec::new();
381
382 let Some(usages_for_fixture) = self.usage_by_fixture.get(&definition.name) else {
384 info!("No references found for fixture: {}", definition.name);
385 return matching_references;
386 };
387
388 for (file_path, usage) in usages_for_fixture.iter() {
389 let fixture_def_at_line = self.get_fixture_definition_at_line(file_path, usage.line);
390
391 let resolved_def = if let Some(ref current_def) = fixture_def_at_line {
392 if current_def.name == usage.name {
393 debug!(
394 "Usage at {:?}:{} is self-referencing, excluding definition at line {}",
395 file_path, usage.line, current_def.line
396 );
397 self.find_closest_definition_excluding(
398 file_path,
399 &usage.name,
400 Some(current_def),
401 )
402 } else {
403 self.find_closest_definition(file_path, &usage.name)
404 }
405 } else {
406 self.find_closest_definition(file_path, &usage.name)
407 };
408
409 if let Some(resolved_def) = resolved_def {
410 if resolved_def == *definition {
411 debug!(
412 "Usage at {:?}:{} resolves to our definition",
413 file_path, usage.line
414 );
415 matching_references.push(usage.clone());
416 } else {
417 debug!(
418 "Usage at {:?}:{} resolves to different definition at {:?}:{}",
419 file_path, usage.line, resolved_def.file_path, resolved_def.line
420 );
421 }
422 }
423 }
424
425 info!(
426 "Found {} references that resolve to this specific definition",
427 matching_references.len()
428 );
429 matching_references
430 }
431
432 pub fn get_undeclared_fixtures(&self, file_path: &Path) -> Vec<UndeclaredFixture> {
434 self.undeclared_fixtures
435 .get(file_path)
436 .map(|entry| entry.value().clone())
437 .unwrap_or_default()
438 }
439
440 pub fn get_available_fixtures(&self, file_path: &Path) -> Vec<FixtureDefinition> {
444 use std::sync::Arc;
445
446 let file_path = self.get_canonical_path(file_path.to_path_buf());
448
449 let current_version = self
451 .definitions_version
452 .load(std::sync::atomic::Ordering::SeqCst);
453
454 if let Some(cached) = self.available_fixtures_cache.get(&file_path) {
455 let (cached_version, cached_fixtures) = cached.value();
456 if *cached_version == current_version {
457 return cached_fixtures.as_ref().clone();
459 }
460 }
461
462 let available_fixtures = self.compute_available_fixtures(&file_path);
464
465 self.available_fixtures_cache.insert(
467 file_path,
468 (current_version, Arc::new(available_fixtures.clone())),
469 );
470
471 available_fixtures
472 }
473
474 fn compute_available_fixtures(&self, file_path: &Path) -> Vec<FixtureDefinition> {
476 let mut available_fixtures = Vec::new();
477 let mut seen_names = HashSet::new();
478
479 for entry in self.definitions.iter() {
481 let fixture_name = entry.key();
482 for def in entry.value().iter() {
483 if def.file_path == file_path && !seen_names.contains(fixture_name.as_str()) {
484 available_fixtures.push(def.clone());
485 seen_names.insert(fixture_name.clone());
486 }
487 }
488 }
489
490 if let Some(mut current_dir) = file_path.parent() {
492 loop {
493 let conftest_path = current_dir.join("conftest.py");
494
495 for entry in self.definitions.iter() {
497 let fixture_name = entry.key();
498 for def in entry.value().iter() {
499 if def.file_path == conftest_path
500 && !seen_names.contains(fixture_name.as_str())
501 {
502 available_fixtures.push(def.clone());
503 seen_names.insert(fixture_name.clone());
504 }
505 }
506 }
507
508 if self.file_cache.contains_key(&conftest_path) {
510 let mut visited = HashSet::new();
511 let imported_fixtures =
512 self.get_imported_fixtures(&conftest_path, &mut visited);
513 for fixture_name in imported_fixtures {
514 if !seen_names.contains(&fixture_name) {
515 if let Some(definitions) = self.definitions.get(&fixture_name) {
517 if let Some(def) = definitions.first() {
518 available_fixtures.push(def.clone());
519 seen_names.insert(fixture_name);
520 }
521 }
522 }
523 }
524 }
525
526 match current_dir.parent() {
527 Some(parent) => current_dir = parent,
528 None => break,
529 }
530 }
531 }
532
533 for entry in self.definitions.iter() {
535 let fixture_name = entry.key();
536 for def in entry.value().iter() {
537 if def.is_third_party && !seen_names.contains(fixture_name.as_str()) {
538 available_fixtures.push(def.clone());
539 seen_names.insert(fixture_name.clone());
540 }
541 }
542 }
543
544 available_fixtures.sort_by(|a, b| a.name.cmp(&b.name));
545 available_fixtures
546 }
547
548 pub fn get_completion_context(
550 &self,
551 file_path: &Path,
552 line: u32,
553 character: u32,
554 ) -> Option<CompletionContext> {
555 let content = self.get_file_content(file_path)?;
556 let target_line = (line + 1) as usize;
557 let line_index = self.get_line_index(file_path, &content);
558
559 let parsed = self.get_parsed_ast(file_path, &content)?;
560
561 if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
562 if let Some(ctx) =
564 self.check_decorator_context(&module.body, &content, target_line, &line_index)
565 {
566 return Some(ctx);
567 }
568
569 return self.get_function_completion_context(
571 &module.body,
572 &content,
573 target_line,
574 character as usize,
575 &line_index,
576 );
577 }
578
579 None
580 }
581
582 fn check_decorator_context(
584 &self,
585 stmts: &[Stmt],
586 _content: &str,
587 target_line: usize,
588 line_index: &[usize],
589 ) -> Option<CompletionContext> {
590 for stmt in stmts {
591 let decorator_list = match stmt {
592 Stmt::FunctionDef(f) => &f.decorator_list,
593 Stmt::AsyncFunctionDef(f) => &f.decorator_list,
594 Stmt::ClassDef(c) => &c.decorator_list,
595 _ => continue,
596 };
597
598 for decorator in decorator_list {
599 let dec_start_line =
600 self.get_line_from_offset(decorator.range().start().to_usize(), line_index);
601 let dec_end_line =
602 self.get_line_from_offset(decorator.range().end().to_usize(), line_index);
603
604 if target_line >= dec_start_line && target_line <= dec_end_line {
605 if decorators::is_usefixtures_decorator(decorator) {
606 return Some(CompletionContext::UsefixuturesDecorator);
607 }
608 if decorators::is_parametrize_decorator(decorator) {
609 return Some(CompletionContext::ParametrizeIndirect);
610 }
611 }
612 }
613
614 if let Stmt::ClassDef(class_def) = stmt {
616 if let Some(ctx) =
617 self.check_decorator_context(&class_def.body, _content, target_line, line_index)
618 {
619 return Some(ctx);
620 }
621 }
622 }
623
624 None
625 }
626
627 fn get_function_completion_context(
629 &self,
630 stmts: &[Stmt],
631 content: &str,
632 target_line: usize,
633 target_char: usize,
634 line_index: &[usize],
635 ) -> Option<CompletionContext> {
636 for stmt in stmts {
637 match stmt {
638 Stmt::FunctionDef(func_def) => {
639 if let Some(ctx) = self.get_func_context(
640 &func_def.name,
641 &func_def.decorator_list,
642 &func_def.args,
643 func_def.range,
644 content,
645 target_line,
646 target_char,
647 line_index,
648 ) {
649 return Some(ctx);
650 }
651 }
652 Stmt::AsyncFunctionDef(func_def) => {
653 if let Some(ctx) = self.get_func_context(
654 &func_def.name,
655 &func_def.decorator_list,
656 &func_def.args,
657 func_def.range,
658 content,
659 target_line,
660 target_char,
661 line_index,
662 ) {
663 return Some(ctx);
664 }
665 }
666 Stmt::ClassDef(class_def) => {
667 if let Some(ctx) = self.get_function_completion_context(
668 &class_def.body,
669 content,
670 target_line,
671 target_char,
672 line_index,
673 ) {
674 return Some(ctx);
675 }
676 }
677 _ => {}
678 }
679 }
680
681 None
682 }
683
684 #[allow(clippy::too_many_arguments)]
686 fn get_func_context(
687 &self,
688 func_name: &rustpython_parser::ast::Identifier,
689 decorator_list: &[Expr],
690 args: &rustpython_parser::ast::Arguments,
691 range: rustpython_parser::text_size::TextRange,
692 content: &str,
693 target_line: usize,
694 _target_char: usize,
695 line_index: &[usize],
696 ) -> Option<CompletionContext> {
697 let func_start_line = self.get_line_from_offset(range.start().to_usize(), line_index);
698 let func_end_line = self.get_line_from_offset(range.end().to_usize(), line_index);
699
700 if target_line < func_start_line || target_line > func_end_line {
701 return None;
702 }
703
704 let is_fixture = decorator_list.iter().any(decorators::is_fixture_decorator);
705 let is_test = func_name.as_str().starts_with("test_");
706
707 if !is_test && !is_fixture {
708 return None;
709 }
710
711 let params: Vec<String> = FixtureDatabase::all_args(args)
713 .map(|arg| arg.def.arg.to_string())
714 .collect();
715
716 let lines: Vec<&str> = content.lines().collect();
718
719 let mut sig_end_line = func_start_line;
720 for (i, line) in lines
721 .iter()
722 .enumerate()
723 .skip(func_start_line.saturating_sub(1))
724 {
725 if line.contains("):") {
726 sig_end_line = i + 1;
727 break;
728 }
729 if i + 1 > func_start_line + 10 {
730 break;
731 }
732 }
733
734 let in_signature = target_line <= sig_end_line;
735
736 let context = if in_signature {
737 CompletionContext::FunctionSignature {
738 function_name: func_name.to_string(),
739 function_line: func_start_line,
740 is_fixture,
741 declared_params: params,
742 }
743 } else {
744 CompletionContext::FunctionBody {
745 function_name: func_name.to_string(),
746 function_line: func_start_line,
747 is_fixture,
748 declared_params: params,
749 }
750 };
751
752 Some(context)
753 }
754
755 pub fn get_function_param_insertion_info(
757 &self,
758 file_path: &Path,
759 function_line: usize,
760 ) -> Option<ParamInsertionInfo> {
761 let content = self.get_file_content(file_path)?;
762 let lines: Vec<&str> = content.lines().collect();
763
764 for i in (function_line.saturating_sub(1))..lines.len().min(function_line + 10) {
765 let line = lines[i];
766 if let Some(paren_pos) = line.find("):") {
767 let has_params = if let Some(open_pos) = line.find('(') {
768 if open_pos < paren_pos {
769 let params_section = &line[open_pos + 1..paren_pos];
770 !params_section.trim().is_empty()
771 } else {
772 true
773 }
774 } else {
775 let before_close = &line[..paren_pos];
776 if !before_close.trim().is_empty() {
777 true
778 } else {
779 let mut found_params = false;
780 for prev_line in lines.iter().take(i).skip(function_line.saturating_sub(1))
781 {
782 if prev_line.contains('(') {
783 if let Some(open_pos) = prev_line.find('(') {
784 let after_open = &prev_line[open_pos + 1..];
785 if !after_open.trim().is_empty() {
786 found_params = true;
787 break;
788 }
789 }
790 } else if !prev_line.trim().is_empty() {
791 found_params = true;
792 break;
793 }
794 }
795 found_params
796 }
797 };
798
799 return Some(ParamInsertionInfo {
800 line: i + 1,
801 char_pos: paren_pos,
802 needs_comma: has_params,
803 });
804 }
805 }
806
807 None
808 }
809
810 #[allow(dead_code)] #[allow(dead_code)] pub fn is_inside_function(
815 &self,
816 file_path: &Path,
817 line: u32,
818 character: u32,
819 ) -> Option<(String, bool, Vec<String>)> {
820 let content = self.get_file_content(file_path)?;
822
823 let target_line = (line + 1) as usize; let parsed = self.get_parsed_ast(file_path, &content)?;
827
828 if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
829 return self.find_enclosing_function(
830 &module.body,
831 &content,
832 target_line,
833 character as usize,
834 );
835 }
836
837 None
838 }
839
840 #[allow(dead_code)]
841 fn find_enclosing_function(
842 &self,
843 stmts: &[Stmt],
844 content: &str,
845 target_line: usize,
846 _target_char: usize,
847 ) -> Option<(String, bool, Vec<String>)> {
848 let line_index = Self::build_line_index(content);
849
850 for stmt in stmts {
851 match stmt {
852 Stmt::FunctionDef(func_def) => {
853 let func_start_line =
854 self.get_line_from_offset(func_def.range.start().to_usize(), &line_index);
855 let func_end_line =
856 self.get_line_from_offset(func_def.range.end().to_usize(), &line_index);
857
858 if target_line >= func_start_line && target_line <= func_end_line {
860 let is_fixture = func_def
861 .decorator_list
862 .iter()
863 .any(decorators::is_fixture_decorator);
864 let is_test = func_def.name.starts_with("test_");
865
866 if is_test || is_fixture {
868 let params: Vec<String> = func_def
869 .args
870 .args
871 .iter()
872 .map(|arg| arg.def.arg.to_string())
873 .collect();
874
875 return Some((func_def.name.to_string(), is_fixture, params));
876 }
877 }
878 }
879 Stmt::AsyncFunctionDef(func_def) => {
880 let func_start_line =
881 self.get_line_from_offset(func_def.range.start().to_usize(), &line_index);
882 let func_end_line =
883 self.get_line_from_offset(func_def.range.end().to_usize(), &line_index);
884
885 if target_line >= func_start_line && target_line <= func_end_line {
886 let is_fixture = func_def
887 .decorator_list
888 .iter()
889 .any(decorators::is_fixture_decorator);
890 let is_test = func_def.name.starts_with("test_");
891
892 if is_test || is_fixture {
893 let params: Vec<String> = func_def
894 .args
895 .args
896 .iter()
897 .map(|arg| arg.def.arg.to_string())
898 .collect();
899
900 return Some((func_def.name.to_string(), is_fixture, params));
901 }
902 }
903 }
904 _ => {}
905 }
906 }
907
908 None
909 }
910
911 pub fn detect_fixture_cycles(&self) -> std::sync::Arc<Vec<super::types::FixtureCycle>> {
917 use std::sync::Arc;
918
919 let current_version = self
920 .definitions_version
921 .load(std::sync::atomic::Ordering::SeqCst);
922
923 if let Some(cached) = self.cycle_cache.get(&()) {
925 let (cached_version, cached_cycles) = cached.value();
926 if *cached_version == current_version {
927 return Arc::clone(cached_cycles);
928 }
929 }
930
931 let cycles = Arc::new(self.compute_fixture_cycles());
933
934 self.cycle_cache
936 .insert((), (current_version, Arc::clone(&cycles)));
937
938 cycles
939 }
940
941 fn compute_fixture_cycles(&self) -> Vec<super::types::FixtureCycle> {
944 use super::types::FixtureCycle;
945 use std::collections::HashMap;
946
947 let mut dep_graph: HashMap<String, Vec<String>> = HashMap::new();
949 let mut fixture_defs: HashMap<String, FixtureDefinition> = HashMap::new();
950
951 for entry in self.definitions.iter() {
952 let fixture_name = entry.key().clone();
953 if let Some(def) = entry.value().first() {
954 fixture_defs.insert(fixture_name.clone(), def.clone());
955 let valid_deps: Vec<String> = def
957 .dependencies
958 .iter()
959 .filter(|d| self.definitions.contains_key(*d))
960 .cloned()
961 .collect();
962 dep_graph.insert(fixture_name, valid_deps);
963 }
964 }
965
966 let mut cycles = Vec::new();
967 let mut visited: HashSet<String> = HashSet::new();
968 let mut seen_cycles: HashSet<String> = HashSet::new(); for start_fixture in dep_graph.keys() {
972 if visited.contains(start_fixture) {
973 continue;
974 }
975
976 let mut stack: Vec<(String, usize, Vec<String>)> =
978 vec![(start_fixture.clone(), 0, vec![])];
979 let mut rec_stack: HashSet<String> = HashSet::new();
980
981 while let Some((current, idx, mut path)) = stack.pop() {
982 if idx == 0 {
983 if rec_stack.contains(¤t) {
985 let cycle_start_idx = path.iter().position(|f| f == ¤t).unwrap_or(0);
987 let mut cycle_path: Vec<String> = path[cycle_start_idx..].to_vec();
988 cycle_path.push(current.clone());
989
990 let mut cycle_key: Vec<String> =
992 cycle_path[..cycle_path.len() - 1].to_vec();
993 cycle_key.sort();
994 let cycle_key_str = cycle_key.join(",");
995
996 if !seen_cycles.contains(&cycle_key_str) {
997 seen_cycles.insert(cycle_key_str);
998 if let Some(fixture_def) = fixture_defs.get(¤t) {
999 cycles.push(FixtureCycle {
1000 cycle_path,
1001 fixture: fixture_def.clone(),
1002 });
1003 }
1004 }
1005 continue;
1006 }
1007
1008 rec_stack.insert(current.clone());
1009 path.push(current.clone());
1010 }
1011
1012 let deps = match dep_graph.get(¤t) {
1014 Some(d) => d,
1015 None => {
1016 rec_stack.remove(¤t);
1017 continue;
1018 }
1019 };
1020
1021 if idx < deps.len() {
1022 stack.push((current.clone(), idx + 1, path.clone()));
1024
1025 let dep = &deps[idx];
1026 if rec_stack.contains(dep) {
1027 let cycle_start_idx = path.iter().position(|f| f == dep).unwrap_or(0);
1029 let mut cycle_path: Vec<String> = path[cycle_start_idx..].to_vec();
1030 cycle_path.push(dep.clone());
1031
1032 let mut cycle_key: Vec<String> =
1033 cycle_path[..cycle_path.len() - 1].to_vec();
1034 cycle_key.sort();
1035 let cycle_key_str = cycle_key.join(",");
1036
1037 if !seen_cycles.contains(&cycle_key_str) {
1038 seen_cycles.insert(cycle_key_str);
1039 if let Some(fixture_def) = fixture_defs.get(dep) {
1040 cycles.push(FixtureCycle {
1041 cycle_path,
1042 fixture: fixture_def.clone(),
1043 });
1044 }
1045 }
1046 } else if !visited.contains(dep) {
1047 stack.push((dep.clone(), 0, path.clone()));
1049 }
1050 } else {
1051 visited.insert(current.clone());
1053 rec_stack.remove(¤t);
1054 }
1055 }
1056 }
1057
1058 cycles
1059 }
1060
1061 pub fn detect_fixture_cycles_in_file(
1065 &self,
1066 file_path: &Path,
1067 ) -> Vec<super::types::FixtureCycle> {
1068 let all_cycles = self.detect_fixture_cycles();
1069 all_cycles
1070 .iter()
1071 .filter(|cycle| cycle.fixture.file_path == file_path)
1072 .cloned()
1073 .collect()
1074 }
1075
1076 pub fn detect_scope_mismatches_in_file(
1082 &self,
1083 file_path: &Path,
1084 ) -> Vec<super::types::ScopeMismatch> {
1085 use super::types::ScopeMismatch;
1086
1087 let mut mismatches = Vec::new();
1088
1089 let Some(fixture_names) = self.file_definitions.get(file_path) else {
1091 return mismatches;
1092 };
1093
1094 for fixture_name in fixture_names.iter() {
1095 let Some(definitions) = self.definitions.get(fixture_name) else {
1097 continue;
1098 };
1099
1100 let Some(fixture_def) = definitions.iter().find(|d| d.file_path == file_path) else {
1102 continue;
1103 };
1104
1105 for dep_name in &fixture_def.dependencies {
1107 if let Some(dep_definitions) = self.definitions.get(dep_name) {
1109 if let Some(dep_def) = dep_definitions.first() {
1112 if fixture_def.scope > dep_def.scope {
1115 mismatches.push(ScopeMismatch {
1116 fixture: fixture_def.clone(),
1117 dependency: dep_def.clone(),
1118 });
1119 }
1120 }
1121 }
1122 }
1123 }
1124
1125 mismatches
1126 }
1127
1128 pub fn resolve_fixture_for_file(
1133 &self,
1134 file_path: &Path,
1135 fixture_name: &str,
1136 ) -> Option<FixtureDefinition> {
1137 let definitions = self.definitions.get(fixture_name)?;
1138
1139 if let Some(def) = definitions.iter().find(|d| d.file_path == file_path) {
1141 return Some(def.clone());
1142 }
1143
1144 let file_path = self.get_canonical_path(file_path.to_path_buf());
1146 let mut best_conftest: Option<&FixtureDefinition> = None;
1147 let mut best_depth = usize::MAX;
1148
1149 for def in definitions.iter() {
1150 if def.is_third_party {
1151 continue;
1152 }
1153 if def.file_path.ends_with("conftest.py") {
1154 if let Some(parent) = def.file_path.parent() {
1155 if file_path.starts_with(parent) {
1156 let depth = parent.components().count();
1157 if depth > best_depth {
1158 best_conftest = Some(def);
1160 best_depth = depth;
1161 } else if best_conftest.is_none() {
1162 best_conftest = Some(def);
1163 best_depth = depth;
1164 }
1165 }
1166 }
1167 }
1168 }
1169
1170 if let Some(def) = best_conftest {
1171 return Some(def.clone());
1172 }
1173
1174 if let Some(def) = definitions.iter().find(|d| d.is_third_party) {
1176 return Some(def.clone());
1177 }
1178
1179 definitions.first().cloned()
1181 }
1182
1183 pub fn find_containing_function(&self, file_path: &Path, line: usize) -> Option<String> {
1187 let content = self.get_file_content(file_path)?;
1188
1189 let parsed = self.get_parsed_ast(file_path, &content)?;
1191
1192 if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
1193 let line_index = self.get_line_index(file_path, &content);
1195
1196 for stmt in &module.body {
1197 if let Some(name) = self.find_function_containing_line(stmt, line, &line_index) {
1198 return Some(name);
1199 }
1200 }
1201 }
1202
1203 None
1204 }
1205
1206 fn find_function_containing_line(
1208 &self,
1209 stmt: &Stmt,
1210 target_line: usize,
1211 line_index: &[usize],
1212 ) -> Option<String> {
1213 match stmt {
1214 Stmt::FunctionDef(func_def) => {
1215 let start_line =
1216 self.get_line_from_offset(func_def.range.start().to_usize(), line_index);
1217 let end_line =
1218 self.get_line_from_offset(func_def.range.end().to_usize(), line_index);
1219
1220 if target_line >= start_line && target_line <= end_line {
1221 return Some(func_def.name.to_string());
1222 }
1223 }
1224 Stmt::AsyncFunctionDef(func_def) => {
1225 let start_line =
1226 self.get_line_from_offset(func_def.range.start().to_usize(), line_index);
1227 let end_line =
1228 self.get_line_from_offset(func_def.range.end().to_usize(), line_index);
1229
1230 if target_line >= start_line && target_line <= end_line {
1231 return Some(func_def.name.to_string());
1232 }
1233 }
1234 Stmt::ClassDef(class_def) => {
1235 for class_stmt in &class_def.body {
1237 if let Some(name) =
1238 self.find_function_containing_line(class_stmt, target_line, line_index)
1239 {
1240 return Some(name);
1241 }
1242 }
1243 }
1244 _ => {}
1245 }
1246 None
1247 }
1248}