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!(
259 "No fixture {} found in conftest hierarchy, checking plugins",
260 fixture_name
261 );
262 for def in definitions.iter() {
263 if def.is_plugin && !def.is_third_party && filter(def) {
264 info!(
265 "Found plugin fixture {} via pytest11 entry point: {:?}",
266 fixture_name, def.file_path
267 );
268 return Some(def.clone());
269 }
270 }
271
272 debug!(
274 "No fixture {} found in plugins, checking third-party",
275 fixture_name
276 );
277 for def in definitions.iter() {
278 if def.is_third_party && filter(def) {
279 info!(
280 "Found third-party fixture {} in site-packages: {:?}",
281 fixture_name, def.file_path
282 );
283 return Some(def.clone());
284 }
285 }
286
287 debug!(
288 "No fixture {} found in scope for {:?}",
289 fixture_name, file_path
290 );
291 None
292 }
293
294 pub fn find_fixture_at_position(
296 &self,
297 file_path: &Path,
298 line: u32,
299 character: u32,
300 ) -> Option<String> {
301 let target_line = (line + 1) as usize;
302
303 debug!(
304 "find_fixture_at_position: file={:?}, line={}, char={}",
305 file_path, target_line, character
306 );
307
308 let content = self.get_file_content(file_path)?;
309 let line_content = content.lines().nth(target_line.saturating_sub(1))?;
310 debug!("Line content: {}", line_content);
311
312 let word_at_cursor = self.extract_word_at_position(line_content, character as usize);
313 debug!("Word at cursor: {:?}", word_at_cursor);
314
315 if let Some(usages) = self.usages.get(file_path) {
317 for usage in usages.iter() {
318 if usage.line == target_line {
319 let cursor_pos = character as usize;
320 if cursor_pos >= usage.start_char && cursor_pos < usage.end_char {
321 debug!(
322 "Cursor at {} is within usage range {}-{}: {}",
323 cursor_pos, usage.start_char, usage.end_char, usage.name
324 );
325 info!("Found fixture usage at cursor position: {}", usage.name);
326 return Some(usage.name.clone());
327 }
328 }
329 }
330 }
331
332 for entry in self.definitions.iter() {
334 for def in entry.value().iter() {
335 if def.file_path == file_path && def.line == target_line {
336 if let Some(ref word) = word_at_cursor {
337 if word == &def.name {
338 info!(
339 "Found fixture definition name at cursor position: {}",
340 def.name
341 );
342 return Some(def.name.clone());
343 }
344 }
345 }
346 }
347 }
348
349 debug!("No fixture found at cursor position");
350 None
351 }
352
353 pub fn extract_word_at_position(&self, line: &str, character: usize) -> Option<String> {
355 super::string_utils::extract_word_at_position(line, character)
356 }
357
358 pub fn find_fixture_references(&self, fixture_name: &str) -> Vec<FixtureUsage> {
360 info!("Finding all references for fixture: {}", fixture_name);
361
362 let mut all_references = Vec::new();
363
364 for entry in self.usages.iter() {
365 let file_path = entry.key();
366 let usages = entry.value();
367
368 for usage in usages.iter() {
369 if usage.name == fixture_name {
370 debug!(
371 "Found reference to {} in {:?} at line {}",
372 fixture_name, file_path, usage.line
373 );
374 all_references.push(usage.clone());
375 }
376 }
377 }
378
379 info!(
380 "Found {} total references for fixture: {}",
381 all_references.len(),
382 fixture_name
383 );
384 all_references
385 }
386
387 pub fn find_references_for_definition(
391 &self,
392 definition: &FixtureDefinition,
393 ) -> Vec<FixtureUsage> {
394 info!(
395 "Finding references for specific definition: {} at {:?}:{}",
396 definition.name, definition.file_path, definition.line
397 );
398
399 let mut matching_references = Vec::new();
400
401 let Some(usages_for_fixture) = self.usage_by_fixture.get(&definition.name) else {
403 info!("No references found for fixture: {}", definition.name);
404 return matching_references;
405 };
406
407 for (file_path, usage) in usages_for_fixture.iter() {
408 let fixture_def_at_line = self.get_fixture_definition_at_line(file_path, usage.line);
409
410 let resolved_def = if let Some(ref current_def) = fixture_def_at_line {
411 if current_def.name == usage.name {
412 debug!(
413 "Usage at {:?}:{} is self-referencing, excluding definition at line {}",
414 file_path, usage.line, current_def.line
415 );
416 self.find_closest_definition_excluding(
417 file_path,
418 &usage.name,
419 Some(current_def),
420 )
421 } else {
422 self.find_closest_definition(file_path, &usage.name)
423 }
424 } else {
425 self.find_closest_definition(file_path, &usage.name)
426 };
427
428 if let Some(resolved_def) = resolved_def {
429 if resolved_def == *definition {
430 debug!(
431 "Usage at {:?}:{} resolves to our definition",
432 file_path, usage.line
433 );
434 matching_references.push(usage.clone());
435 } else {
436 debug!(
437 "Usage at {:?}:{} resolves to different definition at {:?}:{}",
438 file_path, usage.line, resolved_def.file_path, resolved_def.line
439 );
440 }
441 }
442 }
443
444 info!(
445 "Found {} references that resolve to this specific definition",
446 matching_references.len()
447 );
448 matching_references
449 }
450
451 pub fn get_undeclared_fixtures(&self, file_path: &Path) -> Vec<UndeclaredFixture> {
453 self.undeclared_fixtures
454 .get(file_path)
455 .map(|entry| entry.value().clone())
456 .unwrap_or_default()
457 }
458
459 pub fn get_available_fixtures(&self, file_path: &Path) -> Vec<FixtureDefinition> {
463 use std::sync::Arc;
464
465 let file_path = self.get_canonical_path(file_path.to_path_buf());
467
468 let current_version = self
470 .definitions_version
471 .load(std::sync::atomic::Ordering::SeqCst);
472
473 if let Some(cached) = self.available_fixtures_cache.get(&file_path) {
474 let (cached_version, cached_fixtures) = cached.value();
475 if *cached_version == current_version {
476 return cached_fixtures.as_ref().clone();
478 }
479 }
480
481 let available_fixtures = self.compute_available_fixtures(&file_path);
483
484 self.available_fixtures_cache.insert(
486 file_path,
487 (current_version, Arc::new(available_fixtures.clone())),
488 );
489
490 available_fixtures
491 }
492
493 fn compute_available_fixtures(&self, file_path: &Path) -> Vec<FixtureDefinition> {
495 let mut available_fixtures = Vec::new();
496 let mut seen_names = HashSet::new();
497
498 for entry in self.definitions.iter() {
500 let fixture_name = entry.key();
501 for def in entry.value().iter() {
502 if def.file_path == file_path && !seen_names.contains(fixture_name.as_str()) {
503 available_fixtures.push(def.clone());
504 seen_names.insert(fixture_name.clone());
505 }
506 }
507 }
508
509 if let Some(mut current_dir) = file_path.parent() {
511 loop {
512 let conftest_path = current_dir.join("conftest.py");
513
514 for entry in self.definitions.iter() {
516 let fixture_name = entry.key();
517 for def in entry.value().iter() {
518 if def.file_path == conftest_path
519 && !seen_names.contains(fixture_name.as_str())
520 {
521 available_fixtures.push(def.clone());
522 seen_names.insert(fixture_name.clone());
523 }
524 }
525 }
526
527 if self.file_cache.contains_key(&conftest_path) {
529 let mut visited = HashSet::new();
530 let imported_fixtures =
531 self.get_imported_fixtures(&conftest_path, &mut visited);
532 for fixture_name in imported_fixtures {
533 if !seen_names.contains(&fixture_name) {
534 if let Some(definitions) = self.definitions.get(&fixture_name) {
536 if let Some(def) = definitions.first() {
537 available_fixtures.push(def.clone());
538 seen_names.insert(fixture_name);
539 }
540 }
541 }
542 }
543 }
544
545 match current_dir.parent() {
546 Some(parent) => current_dir = parent,
547 None => break,
548 }
549 }
550 }
551
552 for entry in self.definitions.iter() {
554 let fixture_name = entry.key();
555 for def in entry.value().iter() {
556 if def.is_plugin
557 && !def.is_third_party
558 && !seen_names.contains(fixture_name.as_str())
559 {
560 available_fixtures.push(def.clone());
561 seen_names.insert(fixture_name.clone());
562 }
563 }
564 }
565
566 for entry in self.definitions.iter() {
568 let fixture_name = entry.key();
569 for def in entry.value().iter() {
570 if def.is_third_party && !seen_names.contains(fixture_name.as_str()) {
571 available_fixtures.push(def.clone());
572 seen_names.insert(fixture_name.clone());
573 }
574 }
575 }
576
577 available_fixtures.sort_by(|a, b| a.name.cmp(&b.name));
578 available_fixtures
579 }
580
581 pub fn get_completion_context(
583 &self,
584 file_path: &Path,
585 line: u32,
586 character: u32,
587 ) -> Option<CompletionContext> {
588 let content = self.get_file_content(file_path)?;
589 let target_line = (line + 1) as usize;
590 let line_index = self.get_line_index(file_path, &content);
591
592 let parsed = self.get_parsed_ast(file_path, &content)?;
593
594 if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
595 if let Some(ctx) =
597 self.check_decorator_context(&module.body, &content, target_line, &line_index)
598 {
599 return Some(ctx);
600 }
601
602 return self.get_function_completion_context(
604 &module.body,
605 &content,
606 target_line,
607 character as usize,
608 &line_index,
609 );
610 }
611
612 None
613 }
614
615 fn check_decorator_context(
618 &self,
619 stmts: &[Stmt],
620 _content: &str,
621 target_line: usize,
622 line_index: &[usize],
623 ) -> Option<CompletionContext> {
624 for stmt in stmts {
625 let decorator_list = match stmt {
627 Stmt::FunctionDef(f) => Some(f.decorator_list.as_slice()),
628 Stmt::AsyncFunctionDef(f) => Some(f.decorator_list.as_slice()),
629 Stmt::ClassDef(c) => Some(c.decorator_list.as_slice()),
630 _ => None,
631 };
632
633 if let Some(decorator_list) = decorator_list {
634 for decorator in decorator_list {
635 let dec_start_line =
636 self.get_line_from_offset(decorator.range().start().to_usize(), line_index);
637 let dec_end_line =
638 self.get_line_from_offset(decorator.range().end().to_usize(), line_index);
639
640 if target_line >= dec_start_line && target_line <= dec_end_line {
641 if decorators::is_usefixtures_decorator(decorator) {
642 return Some(CompletionContext::UsefixuturesDecorator);
643 }
644 if decorators::is_parametrize_decorator(decorator) {
645 return Some(CompletionContext::ParametrizeIndirect);
646 }
647 }
648 }
649 }
650
651 let pytestmark_value: Option<&Expr> = match stmt {
653 Stmt::Assign(assign) => {
654 let is_pytestmark = assign
655 .targets
656 .iter()
657 .any(|t| matches!(t, Expr::Name(n) if n.id.as_str() == "pytestmark"));
658 if is_pytestmark {
659 Some(assign.value.as_ref())
660 } else {
661 None
662 }
663 }
664 Stmt::AnnAssign(ann_assign) => {
665 let is_pytestmark = matches!(
666 ann_assign.target.as_ref(),
667 Expr::Name(n) if n.id.as_str() == "pytestmark"
668 );
669 if is_pytestmark {
670 ann_assign.value.as_ref().map(|v| v.as_ref())
671 } else {
672 None
673 }
674 }
675 _ => None,
676 };
677
678 if let Some(value) = pytestmark_value {
679 let stmt_start =
680 self.get_line_from_offset(stmt.range().start().to_usize(), line_index);
681 let stmt_end = self.get_line_from_offset(stmt.range().end().to_usize(), line_index);
682
683 if target_line >= stmt_start
684 && target_line <= stmt_end
685 && self.cursor_inside_usefixtures_call(value, target_line, line_index)
686 {
687 return Some(CompletionContext::UsefixuturesDecorator);
688 }
689 }
690
691 if let Stmt::ClassDef(class_def) = stmt {
693 if let Some(ctx) =
694 self.check_decorator_context(&class_def.body, _content, target_line, line_index)
695 {
696 return Some(ctx);
697 }
698 }
699 }
700
701 None
702 }
703
704 fn cursor_inside_usefixtures_call(
707 &self,
708 expr: &Expr,
709 target_line: usize,
710 line_index: &[usize],
711 ) -> bool {
712 match expr {
713 Expr::Call(call) => {
714 if decorators::is_usefixtures_decorator(&call.func) {
715 let call_start =
716 self.get_line_from_offset(expr.range().start().to_usize(), line_index);
717 let call_end =
718 self.get_line_from_offset(expr.range().end().to_usize(), line_index);
719 return target_line >= call_start && target_line <= call_end;
720 }
721 false
722 }
723 Expr::List(list) => list
724 .elts
725 .iter()
726 .any(|e| self.cursor_inside_usefixtures_call(e, target_line, line_index)),
727 Expr::Tuple(tuple) => tuple
728 .elts
729 .iter()
730 .any(|e| self.cursor_inside_usefixtures_call(e, target_line, line_index)),
731 _ => false,
732 }
733 }
734
735 fn get_function_completion_context(
737 &self,
738 stmts: &[Stmt],
739 content: &str,
740 target_line: usize,
741 target_char: usize,
742 line_index: &[usize],
743 ) -> Option<CompletionContext> {
744 for stmt in stmts {
745 match stmt {
746 Stmt::FunctionDef(func_def) => {
747 if let Some(ctx) = self.get_func_context(
748 &func_def.name,
749 &func_def.decorator_list,
750 &func_def.args,
751 func_def.range,
752 content,
753 target_line,
754 target_char,
755 line_index,
756 ) {
757 return Some(ctx);
758 }
759 }
760 Stmt::AsyncFunctionDef(func_def) => {
761 if let Some(ctx) = self.get_func_context(
762 &func_def.name,
763 &func_def.decorator_list,
764 &func_def.args,
765 func_def.range,
766 content,
767 target_line,
768 target_char,
769 line_index,
770 ) {
771 return Some(ctx);
772 }
773 }
774 Stmt::ClassDef(class_def) => {
775 if let Some(ctx) = self.get_function_completion_context(
776 &class_def.body,
777 content,
778 target_line,
779 target_char,
780 line_index,
781 ) {
782 return Some(ctx);
783 }
784 }
785 _ => {}
786 }
787 }
788
789 None
790 }
791
792 #[allow(clippy::too_many_arguments)]
794 fn get_func_context(
795 &self,
796 func_name: &rustpython_parser::ast::Identifier,
797 decorator_list: &[Expr],
798 args: &rustpython_parser::ast::Arguments,
799 range: rustpython_parser::text_size::TextRange,
800 content: &str,
801 target_line: usize,
802 _target_char: usize,
803 line_index: &[usize],
804 ) -> Option<CompletionContext> {
805 let func_start_line = self.get_line_from_offset(range.start().to_usize(), line_index);
806 let func_end_line = self.get_line_from_offset(range.end().to_usize(), line_index);
807
808 if target_line < func_start_line || target_line > func_end_line {
809 return None;
810 }
811
812 let is_fixture = decorator_list.iter().any(decorators::is_fixture_decorator);
813 let is_test = func_name.as_str().starts_with("test_");
814
815 if !is_test && !is_fixture {
816 return None;
817 }
818
819 let params: Vec<String> = FixtureDatabase::all_args(args)
821 .map(|arg| arg.def.arg.to_string())
822 .collect();
823
824 let lines: Vec<&str> = content.lines().collect();
826
827 let mut sig_end_line = func_start_line;
828 for (i, line) in lines
829 .iter()
830 .enumerate()
831 .skip(func_start_line.saturating_sub(1))
832 {
833 if line.contains("):") {
834 sig_end_line = i + 1;
835 break;
836 }
837 if i + 1 > func_start_line + 10 {
838 break;
839 }
840 }
841
842 let in_signature = target_line <= sig_end_line;
843
844 let context = if in_signature {
845 CompletionContext::FunctionSignature {
846 function_name: func_name.to_string(),
847 function_line: func_start_line,
848 is_fixture,
849 declared_params: params,
850 }
851 } else {
852 CompletionContext::FunctionBody {
853 function_name: func_name.to_string(),
854 function_line: func_start_line,
855 is_fixture,
856 declared_params: params,
857 }
858 };
859
860 Some(context)
861 }
862
863 pub fn get_function_param_insertion_info(
865 &self,
866 file_path: &Path,
867 function_line: usize,
868 ) -> Option<ParamInsertionInfo> {
869 let content = self.get_file_content(file_path)?;
870 let lines: Vec<&str> = content.lines().collect();
871
872 for i in (function_line.saturating_sub(1))..lines.len().min(function_line + 10) {
873 let line = lines[i];
874 if let Some(paren_pos) = line.find("):") {
875 let has_params = if let Some(open_pos) = line.find('(') {
876 if open_pos < paren_pos {
877 let params_section = &line[open_pos + 1..paren_pos];
878 !params_section.trim().is_empty()
879 } else {
880 true
881 }
882 } else {
883 let before_close = &line[..paren_pos];
884 if !before_close.trim().is_empty() {
885 true
886 } else {
887 let mut found_params = false;
888 for prev_line in lines.iter().take(i).skip(function_line.saturating_sub(1))
889 {
890 if prev_line.contains('(') {
891 if let Some(open_pos) = prev_line.find('(') {
892 let after_open = &prev_line[open_pos + 1..];
893 if !after_open.trim().is_empty() {
894 found_params = true;
895 break;
896 }
897 }
898 } else if !prev_line.trim().is_empty() {
899 found_params = true;
900 break;
901 }
902 }
903 found_params
904 }
905 };
906
907 return Some(ParamInsertionInfo {
908 line: i + 1,
909 char_pos: paren_pos,
910 needs_comma: has_params,
911 });
912 }
913 }
914
915 None
916 }
917
918 #[allow(dead_code)] #[allow(dead_code)] pub fn is_inside_function(
923 &self,
924 file_path: &Path,
925 line: u32,
926 character: u32,
927 ) -> Option<(String, bool, Vec<String>)> {
928 let content = self.get_file_content(file_path)?;
930
931 let target_line = (line + 1) as usize; let parsed = self.get_parsed_ast(file_path, &content)?;
935
936 if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
937 return self.find_enclosing_function(
938 &module.body,
939 &content,
940 target_line,
941 character as usize,
942 );
943 }
944
945 None
946 }
947
948 #[allow(dead_code)]
949 fn find_enclosing_function(
950 &self,
951 stmts: &[Stmt],
952 content: &str,
953 target_line: usize,
954 _target_char: usize,
955 ) -> Option<(String, bool, Vec<String>)> {
956 let line_index = Self::build_line_index(content);
957
958 for stmt in stmts {
959 match stmt {
960 Stmt::FunctionDef(func_def) => {
961 let func_start_line =
962 self.get_line_from_offset(func_def.range.start().to_usize(), &line_index);
963 let func_end_line =
964 self.get_line_from_offset(func_def.range.end().to_usize(), &line_index);
965
966 if target_line >= func_start_line && target_line <= func_end_line {
968 let is_fixture = func_def
969 .decorator_list
970 .iter()
971 .any(decorators::is_fixture_decorator);
972 let is_test = func_def.name.starts_with("test_");
973
974 if is_test || is_fixture {
976 let params: Vec<String> = func_def
977 .args
978 .args
979 .iter()
980 .map(|arg| arg.def.arg.to_string())
981 .collect();
982
983 return Some((func_def.name.to_string(), is_fixture, params));
984 }
985 }
986 }
987 Stmt::AsyncFunctionDef(func_def) => {
988 let func_start_line =
989 self.get_line_from_offset(func_def.range.start().to_usize(), &line_index);
990 let func_end_line =
991 self.get_line_from_offset(func_def.range.end().to_usize(), &line_index);
992
993 if target_line >= func_start_line && target_line <= func_end_line {
994 let is_fixture = func_def
995 .decorator_list
996 .iter()
997 .any(decorators::is_fixture_decorator);
998 let is_test = func_def.name.starts_with("test_");
999
1000 if is_test || is_fixture {
1001 let params: Vec<String> = func_def
1002 .args
1003 .args
1004 .iter()
1005 .map(|arg| arg.def.arg.to_string())
1006 .collect();
1007
1008 return Some((func_def.name.to_string(), is_fixture, params));
1009 }
1010 }
1011 }
1012 _ => {}
1013 }
1014 }
1015
1016 None
1017 }
1018
1019 pub fn detect_fixture_cycles(&self) -> std::sync::Arc<Vec<super::types::FixtureCycle>> {
1025 use std::sync::Arc;
1026
1027 let current_version = self
1028 .definitions_version
1029 .load(std::sync::atomic::Ordering::SeqCst);
1030
1031 if let Some(cached) = self.cycle_cache.get(&()) {
1033 let (cached_version, cached_cycles) = cached.value();
1034 if *cached_version == current_version {
1035 return Arc::clone(cached_cycles);
1036 }
1037 }
1038
1039 let cycles = Arc::new(self.compute_fixture_cycles());
1041
1042 self.cycle_cache
1044 .insert((), (current_version, Arc::clone(&cycles)));
1045
1046 cycles
1047 }
1048
1049 fn compute_fixture_cycles(&self) -> Vec<super::types::FixtureCycle> {
1052 use super::types::FixtureCycle;
1053 use std::collections::HashMap;
1054
1055 let mut dep_graph: HashMap<String, Vec<String>> = HashMap::new();
1057 let mut fixture_defs: HashMap<String, FixtureDefinition> = HashMap::new();
1058
1059 for entry in self.definitions.iter() {
1060 let fixture_name = entry.key().clone();
1061 if let Some(def) = entry.value().first() {
1062 fixture_defs.insert(fixture_name.clone(), def.clone());
1063 let valid_deps: Vec<String> = def
1065 .dependencies
1066 .iter()
1067 .filter(|d| self.definitions.contains_key(*d))
1068 .cloned()
1069 .collect();
1070 dep_graph.insert(fixture_name, valid_deps);
1071 }
1072 }
1073
1074 let mut cycles = Vec::new();
1075 let mut visited: HashSet<String> = HashSet::new();
1076 let mut seen_cycles: HashSet<String> = HashSet::new(); for start_fixture in dep_graph.keys() {
1080 if visited.contains(start_fixture) {
1081 continue;
1082 }
1083
1084 let mut stack: Vec<(String, usize, Vec<String>)> =
1086 vec![(start_fixture.clone(), 0, vec![])];
1087 let mut rec_stack: HashSet<String> = HashSet::new();
1088
1089 while let Some((current, idx, mut path)) = stack.pop() {
1090 if idx == 0 {
1091 if rec_stack.contains(¤t) {
1093 let cycle_start_idx = path.iter().position(|f| f == ¤t).unwrap_or(0);
1095 let mut cycle_path: Vec<String> = path[cycle_start_idx..].to_vec();
1096 cycle_path.push(current.clone());
1097
1098 let mut cycle_key: Vec<String> =
1100 cycle_path[..cycle_path.len() - 1].to_vec();
1101 cycle_key.sort();
1102 let cycle_key_str = cycle_key.join(",");
1103
1104 if !seen_cycles.contains(&cycle_key_str) {
1105 seen_cycles.insert(cycle_key_str);
1106 if let Some(fixture_def) = fixture_defs.get(¤t) {
1107 cycles.push(FixtureCycle {
1108 cycle_path,
1109 fixture: fixture_def.clone(),
1110 });
1111 }
1112 }
1113 continue;
1114 }
1115
1116 rec_stack.insert(current.clone());
1117 path.push(current.clone());
1118 }
1119
1120 let deps = match dep_graph.get(¤t) {
1122 Some(d) => d,
1123 None => {
1124 rec_stack.remove(¤t);
1125 continue;
1126 }
1127 };
1128
1129 if idx < deps.len() {
1130 stack.push((current.clone(), idx + 1, path.clone()));
1132
1133 let dep = &deps[idx];
1134 if rec_stack.contains(dep) {
1135 let cycle_start_idx = path.iter().position(|f| f == dep).unwrap_or(0);
1137 let mut cycle_path: Vec<String> = path[cycle_start_idx..].to_vec();
1138 cycle_path.push(dep.clone());
1139
1140 let mut cycle_key: Vec<String> =
1141 cycle_path[..cycle_path.len() - 1].to_vec();
1142 cycle_key.sort();
1143 let cycle_key_str = cycle_key.join(",");
1144
1145 if !seen_cycles.contains(&cycle_key_str) {
1146 seen_cycles.insert(cycle_key_str);
1147 if let Some(fixture_def) = fixture_defs.get(dep) {
1148 cycles.push(FixtureCycle {
1149 cycle_path,
1150 fixture: fixture_def.clone(),
1151 });
1152 }
1153 }
1154 } else if !visited.contains(dep) {
1155 stack.push((dep.clone(), 0, path.clone()));
1157 }
1158 } else {
1159 visited.insert(current.clone());
1161 rec_stack.remove(¤t);
1162 }
1163 }
1164 }
1165
1166 cycles
1167 }
1168
1169 pub fn detect_fixture_cycles_in_file(
1173 &self,
1174 file_path: &Path,
1175 ) -> Vec<super::types::FixtureCycle> {
1176 let all_cycles = self.detect_fixture_cycles();
1177 all_cycles
1178 .iter()
1179 .filter(|cycle| cycle.fixture.file_path == file_path)
1180 .cloned()
1181 .collect()
1182 }
1183
1184 pub fn detect_scope_mismatches_in_file(
1190 &self,
1191 file_path: &Path,
1192 ) -> Vec<super::types::ScopeMismatch> {
1193 use super::types::ScopeMismatch;
1194
1195 let mut mismatches = Vec::new();
1196
1197 let Some(fixture_names) = self.file_definitions.get(file_path) else {
1199 return mismatches;
1200 };
1201
1202 for fixture_name in fixture_names.iter() {
1203 let Some(definitions) = self.definitions.get(fixture_name) else {
1205 continue;
1206 };
1207
1208 let Some(fixture_def) = definitions.iter().find(|d| d.file_path == file_path) else {
1210 continue;
1211 };
1212
1213 for dep_name in &fixture_def.dependencies {
1215 if let Some(dep_definitions) = self.definitions.get(dep_name) {
1217 if let Some(dep_def) = dep_definitions.first() {
1220 if fixture_def.scope > dep_def.scope {
1223 mismatches.push(ScopeMismatch {
1224 fixture: fixture_def.clone(),
1225 dependency: dep_def.clone(),
1226 });
1227 }
1228 }
1229 }
1230 }
1231 }
1232
1233 mismatches
1234 }
1235
1236 pub fn resolve_fixture_for_file(
1241 &self,
1242 file_path: &Path,
1243 fixture_name: &str,
1244 ) -> Option<FixtureDefinition> {
1245 let definitions = self.definitions.get(fixture_name)?;
1246
1247 if let Some(def) = definitions.iter().find(|d| d.file_path == file_path) {
1249 return Some(def.clone());
1250 }
1251
1252 let file_path = self.get_canonical_path(file_path.to_path_buf());
1254 let mut best_conftest: Option<&FixtureDefinition> = None;
1255 let mut best_depth = usize::MAX;
1256
1257 for def in definitions.iter() {
1258 if def.is_third_party {
1259 continue;
1260 }
1261 if def.file_path.ends_with("conftest.py") {
1262 if let Some(parent) = def.file_path.parent() {
1263 if file_path.starts_with(parent) {
1264 let depth = parent.components().count();
1265 if depth > best_depth {
1266 best_conftest = Some(def);
1268 best_depth = depth;
1269 } else if best_conftest.is_none() {
1270 best_conftest = Some(def);
1271 best_depth = depth;
1272 }
1273 }
1274 }
1275 }
1276 }
1277
1278 if let Some(def) = best_conftest {
1279 return Some(def.clone());
1280 }
1281
1282 if let Some(def) = definitions
1284 .iter()
1285 .find(|d| d.is_plugin && !d.is_third_party)
1286 {
1287 return Some(def.clone());
1288 }
1289
1290 if let Some(def) = definitions.iter().find(|d| d.is_third_party) {
1292 return Some(def.clone());
1293 }
1294
1295 definitions.first().cloned()
1297 }
1298
1299 pub fn find_containing_function(&self, file_path: &Path, line: usize) -> Option<String> {
1303 let content = self.get_file_content(file_path)?;
1304
1305 let parsed = self.get_parsed_ast(file_path, &content)?;
1307
1308 if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
1309 let line_index = self.get_line_index(file_path, &content);
1311
1312 for stmt in &module.body {
1313 if let Some(name) = self.find_function_containing_line(stmt, line, &line_index) {
1314 return Some(name);
1315 }
1316 }
1317 }
1318
1319 None
1320 }
1321
1322 fn find_function_containing_line(
1324 &self,
1325 stmt: &Stmt,
1326 target_line: usize,
1327 line_index: &[usize],
1328 ) -> Option<String> {
1329 match stmt {
1330 Stmt::FunctionDef(func_def) => {
1331 let start_line =
1332 self.get_line_from_offset(func_def.range.start().to_usize(), line_index);
1333 let end_line =
1334 self.get_line_from_offset(func_def.range.end().to_usize(), line_index);
1335
1336 if target_line >= start_line && target_line <= end_line {
1337 return Some(func_def.name.to_string());
1338 }
1339 }
1340 Stmt::AsyncFunctionDef(func_def) => {
1341 let start_line =
1342 self.get_line_from_offset(func_def.range.start().to_usize(), line_index);
1343 let end_line =
1344 self.get_line_from_offset(func_def.range.end().to_usize(), line_index);
1345
1346 if target_line >= start_line && target_line <= end_line {
1347 return Some(func_def.name.to_string());
1348 }
1349 }
1350 Stmt::ClassDef(class_def) => {
1351 for class_stmt in &class_def.body {
1353 if let Some(name) =
1354 self.find_function_containing_line(class_stmt, target_line, line_index)
1355 {
1356 return Some(name);
1357 }
1358 }
1359 }
1360 _ => {}
1361 }
1362 None
1363 }
1364}