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