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 let conftest_in_cache = self.file_cache.contains_key(&conftest_path);
230 if (conftest_path.exists() || conftest_in_cache)
231 && self.is_fixture_imported_in_file(fixture_name, &conftest_path)
232 {
233 debug!(
236 "Fixture {} is imported in conftest.py: {:?}",
237 fixture_name, conftest_path
238 );
239 if let Some(def) = definitions.iter().find(|def| filter(def)) {
241 info!(
242 "Found imported fixture {} via conftest.py: {:?} (original: {:?})",
243 fixture_name, conftest_path, def.file_path
244 );
245 return Some(def.clone());
246 }
247 }
248
249 match current_dir.parent() {
250 Some(parent) => current_dir = parent,
251 None => break,
252 }
253 }
254
255 debug!(
257 "No fixture {} found in conftest hierarchy, checking third-party",
258 fixture_name
259 );
260 for def in definitions.iter() {
261 if def.is_third_party && filter(def) {
262 info!(
263 "Found third-party fixture {} in site-packages: {:?}",
264 fixture_name, def.file_path
265 );
266 return Some(def.clone());
267 }
268 }
269
270 debug!(
271 "No fixture {} found in scope for {:?}",
272 fixture_name, file_path
273 );
274 None
275 }
276
277 pub fn find_fixture_at_position(
279 &self,
280 file_path: &Path,
281 line: u32,
282 character: u32,
283 ) -> Option<String> {
284 let target_line = (line + 1) as usize;
285
286 debug!(
287 "find_fixture_at_position: file={:?}, line={}, char={}",
288 file_path, target_line, character
289 );
290
291 let content = self.get_file_content(file_path)?;
292 let line_content = content.lines().nth(target_line.saturating_sub(1))?;
293 debug!("Line content: {}", line_content);
294
295 let word_at_cursor = self.extract_word_at_position(line_content, character as usize);
296 debug!("Word at cursor: {:?}", word_at_cursor);
297
298 if let Some(usages) = self.usages.get(file_path) {
300 for usage in usages.iter() {
301 if usage.line == target_line {
302 let cursor_pos = character as usize;
303 if cursor_pos >= usage.start_char && cursor_pos < usage.end_char {
304 debug!(
305 "Cursor at {} is within usage range {}-{}: {}",
306 cursor_pos, usage.start_char, usage.end_char, usage.name
307 );
308 info!("Found fixture usage at cursor position: {}", usage.name);
309 return Some(usage.name.clone());
310 }
311 }
312 }
313 }
314
315 for entry in self.definitions.iter() {
317 for def in entry.value().iter() {
318 if def.file_path == file_path && def.line == target_line {
319 if let Some(ref word) = word_at_cursor {
320 if word == &def.name {
321 info!(
322 "Found fixture definition name at cursor position: {}",
323 def.name
324 );
325 return Some(def.name.clone());
326 }
327 }
328 }
329 }
330 }
331
332 debug!("No fixture found at cursor position");
333 None
334 }
335
336 pub fn extract_word_at_position(&self, line: &str, character: usize) -> Option<String> {
338 super::string_utils::extract_word_at_position(line, character)
339 }
340
341 pub fn find_fixture_references(&self, fixture_name: &str) -> Vec<FixtureUsage> {
343 info!("Finding all references for fixture: {}", fixture_name);
344
345 let mut all_references = Vec::new();
346
347 for entry in self.usages.iter() {
348 let file_path = entry.key();
349 let usages = entry.value();
350
351 for usage in usages.iter() {
352 if usage.name == fixture_name {
353 debug!(
354 "Found reference to {} in {:?} at line {}",
355 fixture_name, file_path, usage.line
356 );
357 all_references.push(usage.clone());
358 }
359 }
360 }
361
362 info!(
363 "Found {} total references for fixture: {}",
364 all_references.len(),
365 fixture_name
366 );
367 all_references
368 }
369
370 pub fn find_references_for_definition(
374 &self,
375 definition: &FixtureDefinition,
376 ) -> Vec<FixtureUsage> {
377 info!(
378 "Finding references for specific definition: {} at {:?}:{}",
379 definition.name, definition.file_path, definition.line
380 );
381
382 let mut matching_references = Vec::new();
383
384 let Some(usages_for_fixture) = self.usage_by_fixture.get(&definition.name) else {
386 info!("No references found for fixture: {}", definition.name);
387 return matching_references;
388 };
389
390 for (file_path, usage) in usages_for_fixture.iter() {
391 let fixture_def_at_line = self.get_fixture_definition_at_line(file_path, usage.line);
392
393 let resolved_def = if let Some(ref current_def) = fixture_def_at_line {
394 if current_def.name == usage.name {
395 debug!(
396 "Usage at {:?}:{} is self-referencing, excluding definition at line {}",
397 file_path, usage.line, current_def.line
398 );
399 self.find_closest_definition_excluding(
400 file_path,
401 &usage.name,
402 Some(current_def),
403 )
404 } else {
405 self.find_closest_definition(file_path, &usage.name)
406 }
407 } else {
408 self.find_closest_definition(file_path, &usage.name)
409 };
410
411 if let Some(resolved_def) = resolved_def {
412 if resolved_def == *definition {
413 debug!(
414 "Usage at {:?}:{} resolves to our definition",
415 file_path, usage.line
416 );
417 matching_references.push(usage.clone());
418 } else {
419 debug!(
420 "Usage at {:?}:{} resolves to different definition at {:?}:{}",
421 file_path, usage.line, resolved_def.file_path, resolved_def.line
422 );
423 }
424 }
425 }
426
427 info!(
428 "Found {} references that resolve to this specific definition",
429 matching_references.len()
430 );
431 matching_references
432 }
433
434 pub fn get_undeclared_fixtures(&self, file_path: &Path) -> Vec<UndeclaredFixture> {
436 self.undeclared_fixtures
437 .get(file_path)
438 .map(|entry| entry.value().clone())
439 .unwrap_or_default()
440 }
441
442 pub fn get_available_fixtures(&self, file_path: &Path) -> Vec<FixtureDefinition> {
446 use std::sync::Arc;
447
448 let file_path = self.get_canonical_path(file_path.to_path_buf());
450
451 let current_version = self
453 .definitions_version
454 .load(std::sync::atomic::Ordering::SeqCst);
455
456 if let Some(cached) = self.available_fixtures_cache.get(&file_path) {
457 let (cached_version, cached_fixtures) = cached.value();
458 if *cached_version == current_version {
459 return cached_fixtures.as_ref().clone();
461 }
462 }
463
464 let available_fixtures = self.compute_available_fixtures(&file_path);
466
467 self.available_fixtures_cache.insert(
469 file_path,
470 (current_version, Arc::new(available_fixtures.clone())),
471 );
472
473 available_fixtures
474 }
475
476 fn compute_available_fixtures(&self, file_path: &Path) -> Vec<FixtureDefinition> {
478 let mut available_fixtures = Vec::new();
479 let mut seen_names = HashSet::new();
480
481 for entry in self.definitions.iter() {
483 let fixture_name = entry.key();
484 for def in entry.value().iter() {
485 if def.file_path == file_path && !seen_names.contains(fixture_name.as_str()) {
486 available_fixtures.push(def.clone());
487 seen_names.insert(fixture_name.clone());
488 }
489 }
490 }
491
492 if let Some(mut current_dir) = file_path.parent() {
494 loop {
495 let conftest_path = current_dir.join("conftest.py");
496
497 for entry in self.definitions.iter() {
499 let fixture_name = entry.key();
500 for def in entry.value().iter() {
501 if def.file_path == conftest_path
502 && !seen_names.contains(fixture_name.as_str())
503 {
504 available_fixtures.push(def.clone());
505 seen_names.insert(fixture_name.clone());
506 }
507 }
508 }
509
510 if self.file_cache.contains_key(&conftest_path) {
512 let mut visited = HashSet::new();
513 let imported_fixtures =
514 self.get_imported_fixtures(&conftest_path, &mut visited);
515 for fixture_name in imported_fixtures {
516 if !seen_names.contains(&fixture_name) {
517 if let Some(definitions) = self.definitions.get(&fixture_name) {
519 if let Some(def) = definitions.first() {
520 available_fixtures.push(def.clone());
521 seen_names.insert(fixture_name);
522 }
523 }
524 }
525 }
526 }
527
528 match current_dir.parent() {
529 Some(parent) => current_dir = parent,
530 None => break,
531 }
532 }
533 }
534
535 for entry in self.definitions.iter() {
537 let fixture_name = entry.key();
538 for def in entry.value().iter() {
539 if def.is_third_party && !seen_names.contains(fixture_name.as_str()) {
540 available_fixtures.push(def.clone());
541 seen_names.insert(fixture_name.clone());
542 }
543 }
544 }
545
546 available_fixtures.sort_by(|a, b| a.name.cmp(&b.name));
547 available_fixtures
548 }
549
550 pub fn get_completion_context(
552 &self,
553 file_path: &Path,
554 line: u32,
555 character: u32,
556 ) -> Option<CompletionContext> {
557 let content = self.get_file_content(file_path)?;
558 let target_line = (line + 1) as usize;
559 let line_index = self.get_line_index(file_path, &content);
560
561 let parsed = self.get_parsed_ast(file_path, &content)?;
562
563 if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
564 if let Some(ctx) =
566 self.check_decorator_context(&module.body, &content, target_line, &line_index)
567 {
568 return Some(ctx);
569 }
570
571 return self.get_function_completion_context(
573 &module.body,
574 &content,
575 target_line,
576 character as usize,
577 &line_index,
578 );
579 }
580
581 None
582 }
583
584 fn check_decorator_context(
586 &self,
587 stmts: &[Stmt],
588 _content: &str,
589 target_line: usize,
590 line_index: &[usize],
591 ) -> Option<CompletionContext> {
592 for stmt in stmts {
593 let decorator_list = match stmt {
594 Stmt::FunctionDef(f) => &f.decorator_list,
595 Stmt::AsyncFunctionDef(f) => &f.decorator_list,
596 Stmt::ClassDef(c) => &c.decorator_list,
597 _ => continue,
598 };
599
600 for decorator in decorator_list {
601 let dec_start_line =
602 self.get_line_from_offset(decorator.range().start().to_usize(), line_index);
603 let dec_end_line =
604 self.get_line_from_offset(decorator.range().end().to_usize(), line_index);
605
606 if target_line >= dec_start_line && target_line <= dec_end_line {
607 if decorators::is_usefixtures_decorator(decorator) {
608 return Some(CompletionContext::UsefixuturesDecorator);
609 }
610 if decorators::is_parametrize_decorator(decorator) {
611 return Some(CompletionContext::ParametrizeIndirect);
612 }
613 }
614 }
615
616 if let Stmt::ClassDef(class_def) = stmt {
618 if let Some(ctx) =
619 self.check_decorator_context(&class_def.body, _content, target_line, line_index)
620 {
621 return Some(ctx);
622 }
623 }
624 }
625
626 None
627 }
628
629 fn get_function_completion_context(
631 &self,
632 stmts: &[Stmt],
633 content: &str,
634 target_line: usize,
635 target_char: usize,
636 line_index: &[usize],
637 ) -> Option<CompletionContext> {
638 for stmt in stmts {
639 match stmt {
640 Stmt::FunctionDef(func_def) => {
641 if let Some(ctx) = self.get_func_context(
642 &func_def.name,
643 &func_def.decorator_list,
644 &func_def.args,
645 func_def.range,
646 content,
647 target_line,
648 target_char,
649 line_index,
650 ) {
651 return Some(ctx);
652 }
653 }
654 Stmt::AsyncFunctionDef(func_def) => {
655 if let Some(ctx) = self.get_func_context(
656 &func_def.name,
657 &func_def.decorator_list,
658 &func_def.args,
659 func_def.range,
660 content,
661 target_line,
662 target_char,
663 line_index,
664 ) {
665 return Some(ctx);
666 }
667 }
668 Stmt::ClassDef(class_def) => {
669 if let Some(ctx) = self.get_function_completion_context(
670 &class_def.body,
671 content,
672 target_line,
673 target_char,
674 line_index,
675 ) {
676 return Some(ctx);
677 }
678 }
679 _ => {}
680 }
681 }
682
683 None
684 }
685
686 #[allow(clippy::too_many_arguments)]
688 fn get_func_context(
689 &self,
690 func_name: &rustpython_parser::ast::Identifier,
691 decorator_list: &[Expr],
692 args: &rustpython_parser::ast::Arguments,
693 range: rustpython_parser::text_size::TextRange,
694 content: &str,
695 target_line: usize,
696 _target_char: usize,
697 line_index: &[usize],
698 ) -> Option<CompletionContext> {
699 let func_start_line = self.get_line_from_offset(range.start().to_usize(), line_index);
700 let func_end_line = self.get_line_from_offset(range.end().to_usize(), line_index);
701
702 if target_line < func_start_line || target_line > func_end_line {
703 return None;
704 }
705
706 let is_fixture = decorator_list.iter().any(decorators::is_fixture_decorator);
707 let is_test = func_name.as_str().starts_with("test_");
708
709 if !is_test && !is_fixture {
710 return None;
711 }
712
713 let params: Vec<String> = FixtureDatabase::all_args(args)
715 .map(|arg| arg.def.arg.to_string())
716 .collect();
717
718 let lines: Vec<&str> = content.lines().collect();
720
721 let mut sig_end_line = func_start_line;
722 for (i, line) in lines
723 .iter()
724 .enumerate()
725 .skip(func_start_line.saturating_sub(1))
726 {
727 if line.contains("):") {
728 sig_end_line = i + 1;
729 break;
730 }
731 if i + 1 > func_start_line + 10 {
732 break;
733 }
734 }
735
736 let in_signature = target_line <= sig_end_line;
737
738 let context = if in_signature {
739 CompletionContext::FunctionSignature {
740 function_name: func_name.to_string(),
741 function_line: func_start_line,
742 is_fixture,
743 declared_params: params,
744 }
745 } else {
746 CompletionContext::FunctionBody {
747 function_name: func_name.to_string(),
748 function_line: func_start_line,
749 is_fixture,
750 declared_params: params,
751 }
752 };
753
754 Some(context)
755 }
756
757 pub fn get_function_param_insertion_info(
759 &self,
760 file_path: &Path,
761 function_line: usize,
762 ) -> Option<ParamInsertionInfo> {
763 let content = self.get_file_content(file_path)?;
764 let lines: Vec<&str> = content.lines().collect();
765
766 for i in (function_line.saturating_sub(1))..lines.len().min(function_line + 10) {
767 let line = lines[i];
768 if let Some(paren_pos) = line.find("):") {
769 let has_params = if let Some(open_pos) = line.find('(') {
770 if open_pos < paren_pos {
771 let params_section = &line[open_pos + 1..paren_pos];
772 !params_section.trim().is_empty()
773 } else {
774 true
775 }
776 } else {
777 let before_close = &line[..paren_pos];
778 if !before_close.trim().is_empty() {
779 true
780 } else {
781 let mut found_params = false;
782 for prev_line in lines.iter().take(i).skip(function_line.saturating_sub(1))
783 {
784 if prev_line.contains('(') {
785 if let Some(open_pos) = prev_line.find('(') {
786 let after_open = &prev_line[open_pos + 1..];
787 if !after_open.trim().is_empty() {
788 found_params = true;
789 break;
790 }
791 }
792 } else if !prev_line.trim().is_empty() {
793 found_params = true;
794 break;
795 }
796 }
797 found_params
798 }
799 };
800
801 return Some(ParamInsertionInfo {
802 line: i + 1,
803 char_pos: paren_pos,
804 needs_comma: has_params,
805 });
806 }
807 }
808
809 None
810 }
811
812 #[allow(dead_code)] #[allow(dead_code)] pub fn is_inside_function(
817 &self,
818 file_path: &Path,
819 line: u32,
820 character: u32,
821 ) -> Option<(String, bool, Vec<String>)> {
822 let content = self.get_file_content(file_path)?;
824
825 let target_line = (line + 1) as usize; let parsed = self.get_parsed_ast(file_path, &content)?;
829
830 if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
831 return self.find_enclosing_function(
832 &module.body,
833 &content,
834 target_line,
835 character as usize,
836 );
837 }
838
839 None
840 }
841
842 #[allow(dead_code)]
843 fn find_enclosing_function(
844 &self,
845 stmts: &[Stmt],
846 content: &str,
847 target_line: usize,
848 _target_char: usize,
849 ) -> Option<(String, bool, Vec<String>)> {
850 let line_index = Self::build_line_index(content);
851
852 for stmt in stmts {
853 match stmt {
854 Stmt::FunctionDef(func_def) => {
855 let func_start_line =
856 self.get_line_from_offset(func_def.range.start().to_usize(), &line_index);
857 let func_end_line =
858 self.get_line_from_offset(func_def.range.end().to_usize(), &line_index);
859
860 if target_line >= func_start_line && target_line <= func_end_line {
862 let is_fixture = func_def
863 .decorator_list
864 .iter()
865 .any(decorators::is_fixture_decorator);
866 let is_test = func_def.name.starts_with("test_");
867
868 if is_test || is_fixture {
870 let params: Vec<String> = func_def
871 .args
872 .args
873 .iter()
874 .map(|arg| arg.def.arg.to_string())
875 .collect();
876
877 return Some((func_def.name.to_string(), is_fixture, params));
878 }
879 }
880 }
881 Stmt::AsyncFunctionDef(func_def) => {
882 let func_start_line =
883 self.get_line_from_offset(func_def.range.start().to_usize(), &line_index);
884 let func_end_line =
885 self.get_line_from_offset(func_def.range.end().to_usize(), &line_index);
886
887 if target_line >= func_start_line && target_line <= func_end_line {
888 let is_fixture = func_def
889 .decorator_list
890 .iter()
891 .any(decorators::is_fixture_decorator);
892 let is_test = func_def.name.starts_with("test_");
893
894 if is_test || is_fixture {
895 let params: Vec<String> = func_def
896 .args
897 .args
898 .iter()
899 .map(|arg| arg.def.arg.to_string())
900 .collect();
901
902 return Some((func_def.name.to_string(), is_fixture, params));
903 }
904 }
905 }
906 _ => {}
907 }
908 }
909
910 None
911 }
912
913 pub fn detect_fixture_cycles(&self) -> std::sync::Arc<Vec<super::types::FixtureCycle>> {
919 use std::sync::Arc;
920
921 let current_version = self
922 .definitions_version
923 .load(std::sync::atomic::Ordering::SeqCst);
924
925 if let Some(cached) = self.cycle_cache.get(&()) {
927 let (cached_version, cached_cycles) = cached.value();
928 if *cached_version == current_version {
929 return Arc::clone(cached_cycles);
930 }
931 }
932
933 let cycles = Arc::new(self.compute_fixture_cycles());
935
936 self.cycle_cache
938 .insert((), (current_version, Arc::clone(&cycles)));
939
940 cycles
941 }
942
943 fn compute_fixture_cycles(&self) -> Vec<super::types::FixtureCycle> {
946 use super::types::FixtureCycle;
947 use std::collections::HashMap;
948
949 let mut dep_graph: HashMap<String, Vec<String>> = HashMap::new();
951 let mut fixture_defs: HashMap<String, FixtureDefinition> = HashMap::new();
952
953 for entry in self.definitions.iter() {
954 let fixture_name = entry.key().clone();
955 if let Some(def) = entry.value().first() {
956 fixture_defs.insert(fixture_name.clone(), def.clone());
957 let valid_deps: Vec<String> = def
959 .dependencies
960 .iter()
961 .filter(|d| self.definitions.contains_key(*d))
962 .cloned()
963 .collect();
964 dep_graph.insert(fixture_name, valid_deps);
965 }
966 }
967
968 let mut cycles = Vec::new();
969 let mut visited: HashSet<String> = HashSet::new();
970 let mut seen_cycles: HashSet<String> = HashSet::new(); for start_fixture in dep_graph.keys() {
974 if visited.contains(start_fixture) {
975 continue;
976 }
977
978 let mut stack: Vec<(String, usize, Vec<String>)> =
980 vec![(start_fixture.clone(), 0, vec![])];
981 let mut rec_stack: HashSet<String> = HashSet::new();
982
983 while let Some((current, idx, mut path)) = stack.pop() {
984 if idx == 0 {
985 if rec_stack.contains(¤t) {
987 let cycle_start_idx = path.iter().position(|f| f == ¤t).unwrap_or(0);
989 let mut cycle_path: Vec<String> = path[cycle_start_idx..].to_vec();
990 cycle_path.push(current.clone());
991
992 let mut cycle_key: Vec<String> =
994 cycle_path[..cycle_path.len() - 1].to_vec();
995 cycle_key.sort();
996 let cycle_key_str = cycle_key.join(",");
997
998 if !seen_cycles.contains(&cycle_key_str) {
999 seen_cycles.insert(cycle_key_str);
1000 if let Some(fixture_def) = fixture_defs.get(¤t) {
1001 cycles.push(FixtureCycle {
1002 cycle_path,
1003 fixture: fixture_def.clone(),
1004 });
1005 }
1006 }
1007 continue;
1008 }
1009
1010 rec_stack.insert(current.clone());
1011 path.push(current.clone());
1012 }
1013
1014 let deps = match dep_graph.get(¤t) {
1016 Some(d) => d,
1017 None => {
1018 rec_stack.remove(¤t);
1019 continue;
1020 }
1021 };
1022
1023 if idx < deps.len() {
1024 stack.push((current.clone(), idx + 1, path.clone()));
1026
1027 let dep = &deps[idx];
1028 if rec_stack.contains(dep) {
1029 let cycle_start_idx = path.iter().position(|f| f == dep).unwrap_or(0);
1031 let mut cycle_path: Vec<String> = path[cycle_start_idx..].to_vec();
1032 cycle_path.push(dep.clone());
1033
1034 let mut cycle_key: Vec<String> =
1035 cycle_path[..cycle_path.len() - 1].to_vec();
1036 cycle_key.sort();
1037 let cycle_key_str = cycle_key.join(",");
1038
1039 if !seen_cycles.contains(&cycle_key_str) {
1040 seen_cycles.insert(cycle_key_str);
1041 if let Some(fixture_def) = fixture_defs.get(dep) {
1042 cycles.push(FixtureCycle {
1043 cycle_path,
1044 fixture: fixture_def.clone(),
1045 });
1046 }
1047 }
1048 } else if !visited.contains(dep) {
1049 stack.push((dep.clone(), 0, path.clone()));
1051 }
1052 } else {
1053 visited.insert(current.clone());
1055 rec_stack.remove(¤t);
1056 }
1057 }
1058 }
1059
1060 cycles
1061 }
1062
1063 pub fn detect_fixture_cycles_in_file(
1067 &self,
1068 file_path: &Path,
1069 ) -> Vec<super::types::FixtureCycle> {
1070 let all_cycles = self.detect_fixture_cycles();
1071 all_cycles
1072 .iter()
1073 .filter(|cycle| cycle.fixture.file_path == file_path)
1074 .cloned()
1075 .collect()
1076 }
1077
1078 pub fn detect_scope_mismatches_in_file(
1084 &self,
1085 file_path: &Path,
1086 ) -> Vec<super::types::ScopeMismatch> {
1087 use super::types::ScopeMismatch;
1088
1089 let mut mismatches = Vec::new();
1090
1091 let Some(fixture_names) = self.file_definitions.get(file_path) else {
1093 return mismatches;
1094 };
1095
1096 for fixture_name in fixture_names.iter() {
1097 let Some(definitions) = self.definitions.get(fixture_name) else {
1099 continue;
1100 };
1101
1102 let Some(fixture_def) = definitions.iter().find(|d| d.file_path == file_path) else {
1104 continue;
1105 };
1106
1107 for dep_name in &fixture_def.dependencies {
1109 if let Some(dep_definitions) = self.definitions.get(dep_name) {
1111 if let Some(dep_def) = dep_definitions.first() {
1114 if fixture_def.scope > dep_def.scope {
1117 mismatches.push(ScopeMismatch {
1118 fixture: fixture_def.clone(),
1119 dependency: dep_def.clone(),
1120 });
1121 }
1122 }
1123 }
1124 }
1125 }
1126
1127 mismatches
1128 }
1129
1130 pub fn resolve_fixture_for_file(
1135 &self,
1136 file_path: &Path,
1137 fixture_name: &str,
1138 ) -> Option<FixtureDefinition> {
1139 let definitions = self.definitions.get(fixture_name)?;
1140
1141 if let Some(def) = definitions.iter().find(|d| d.file_path == file_path) {
1143 return Some(def.clone());
1144 }
1145
1146 let file_path = self.get_canonical_path(file_path.to_path_buf());
1148 let mut best_conftest: Option<&FixtureDefinition> = None;
1149 let mut best_depth = usize::MAX;
1150
1151 for def in definitions.iter() {
1152 if def.is_third_party {
1153 continue;
1154 }
1155 if def.file_path.ends_with("conftest.py") {
1156 if let Some(parent) = def.file_path.parent() {
1157 if file_path.starts_with(parent) {
1158 let depth = parent.components().count();
1159 if depth > best_depth {
1160 best_conftest = Some(def);
1162 best_depth = depth;
1163 } else if best_conftest.is_none() {
1164 best_conftest = Some(def);
1165 best_depth = depth;
1166 }
1167 }
1168 }
1169 }
1170 }
1171
1172 if let Some(def) = best_conftest {
1173 return Some(def.clone());
1174 }
1175
1176 if let Some(def) = definitions.iter().find(|d| d.is_third_party) {
1178 return Some(def.clone());
1179 }
1180
1181 definitions.first().cloned()
1183 }
1184
1185 pub fn find_containing_function(&self, file_path: &Path, line: usize) -> Option<String> {
1189 let content = self.get_file_content(file_path)?;
1190
1191 let parsed = self.get_parsed_ast(file_path, &content)?;
1193
1194 if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
1195 let line_index = self.get_line_index(file_path, &content);
1197
1198 for stmt in &module.body {
1199 if let Some(name) = self.find_function_containing_line(stmt, line, &line_index) {
1200 return Some(name);
1201 }
1202 }
1203 }
1204
1205 None
1206 }
1207
1208 fn find_function_containing_line(
1210 &self,
1211 stmt: &Stmt,
1212 target_line: usize,
1213 line_index: &[usize],
1214 ) -> Option<String> {
1215 match stmt {
1216 Stmt::FunctionDef(func_def) => {
1217 let start_line =
1218 self.get_line_from_offset(func_def.range.start().to_usize(), line_index);
1219 let end_line =
1220 self.get_line_from_offset(func_def.range.end().to_usize(), line_index);
1221
1222 if target_line >= start_line && target_line <= end_line {
1223 return Some(func_def.name.to_string());
1224 }
1225 }
1226 Stmt::AsyncFunctionDef(func_def) => {
1227 let start_line =
1228 self.get_line_from_offset(func_def.range.start().to_usize(), line_index);
1229 let end_line =
1230 self.get_line_from_offset(func_def.range.end().to_usize(), line_index);
1231
1232 if target_line >= start_line && target_line <= end_line {
1233 return Some(func_def.name.to_string());
1234 }
1235 }
1236 Stmt::ClassDef(class_def) => {
1237 for class_stmt in &class_def.body {
1239 if let Some(name) =
1240 self.find_function_containing_line(class_stmt, target_line, line_index)
1241 {
1242 return Some(name);
1243 }
1244 }
1245 }
1246 _ => {}
1247 }
1248 None
1249 }
1250}