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 fixture_scope = if is_fixture {
821 let scope = decorator_list
822 .iter()
823 .find_map(decorators::extract_fixture_scope)
824 .unwrap_or(super::types::FixtureScope::Function);
825 Some(scope)
826 } else {
827 None
828 };
829
830 let params: Vec<String> = FixtureDatabase::all_args(args)
832 .map(|arg| arg.def.arg.to_string())
833 .collect();
834
835 let lines: Vec<&str> = content.lines().collect();
837
838 let mut sig_end_line = func_start_line;
839 for (i, line) in lines
840 .iter()
841 .enumerate()
842 .skip(func_start_line.saturating_sub(1))
843 {
844 if line.contains("):") {
845 sig_end_line = i + 1;
846 break;
847 }
848 if i + 1 > func_start_line + 10 {
849 break;
850 }
851 }
852
853 let in_signature = target_line <= sig_end_line;
854
855 let context = if in_signature {
856 CompletionContext::FunctionSignature {
857 function_name: func_name.to_string(),
858 function_line: func_start_line,
859 is_fixture,
860 declared_params: params,
861 fixture_scope,
862 }
863 } else {
864 CompletionContext::FunctionBody {
865 function_name: func_name.to_string(),
866 function_line: func_start_line,
867 is_fixture,
868 declared_params: params,
869 fixture_scope,
870 }
871 };
872
873 Some(context)
874 }
875
876 pub fn get_function_param_insertion_info(
878 &self,
879 file_path: &Path,
880 function_line: usize,
881 ) -> Option<ParamInsertionInfo> {
882 let content = self.get_file_content(file_path)?;
883 let lines: Vec<&str> = content.lines().collect();
884
885 for i in (function_line.saturating_sub(1))..lines.len().min(function_line + 10) {
886 let line = lines[i];
887 if let Some(paren_pos) = line.find("):") {
888 let has_params = if let Some(open_pos) = line.find('(') {
889 if open_pos < paren_pos {
890 let params_section = &line[open_pos + 1..paren_pos];
891 !params_section.trim().is_empty()
892 } else {
893 true
894 }
895 } else {
896 let before_close = &line[..paren_pos];
897 if !before_close.trim().is_empty() {
898 true
899 } else {
900 let mut found_params = false;
901 for prev_line in lines.iter().take(i).skip(function_line.saturating_sub(1))
902 {
903 if prev_line.contains('(') {
904 if let Some(open_pos) = prev_line.find('(') {
905 let after_open = &prev_line[open_pos + 1..];
906 if !after_open.trim().is_empty() {
907 found_params = true;
908 break;
909 }
910 }
911 } else if !prev_line.trim().is_empty() {
912 found_params = true;
913 break;
914 }
915 }
916 found_params
917 }
918 };
919
920 return Some(ParamInsertionInfo {
921 line: i + 1,
922 char_pos: paren_pos,
923 needs_comma: has_params,
924 });
925 }
926 }
927
928 None
929 }
930
931 #[allow(dead_code)] #[allow(dead_code)] pub fn is_inside_function(
936 &self,
937 file_path: &Path,
938 line: u32,
939 character: u32,
940 ) -> Option<(String, bool, Vec<String>)> {
941 let content = self.get_file_content(file_path)?;
943
944 let target_line = (line + 1) as usize; let parsed = self.get_parsed_ast(file_path, &content)?;
948
949 if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
950 return self.find_enclosing_function(
951 &module.body,
952 &content,
953 target_line,
954 character as usize,
955 );
956 }
957
958 None
959 }
960
961 #[allow(dead_code)]
962 fn find_enclosing_function(
963 &self,
964 stmts: &[Stmt],
965 content: &str,
966 target_line: usize,
967 _target_char: usize,
968 ) -> Option<(String, bool, Vec<String>)> {
969 let line_index = Self::build_line_index(content);
970
971 for stmt in stmts {
972 match stmt {
973 Stmt::FunctionDef(func_def) => {
974 let func_start_line =
975 self.get_line_from_offset(func_def.range.start().to_usize(), &line_index);
976 let func_end_line =
977 self.get_line_from_offset(func_def.range.end().to_usize(), &line_index);
978
979 if target_line >= func_start_line && target_line <= func_end_line {
981 let is_fixture = func_def
982 .decorator_list
983 .iter()
984 .any(decorators::is_fixture_decorator);
985 let is_test = func_def.name.starts_with("test_");
986
987 if is_test || is_fixture {
989 let params: Vec<String> = func_def
990 .args
991 .args
992 .iter()
993 .map(|arg| arg.def.arg.to_string())
994 .collect();
995
996 return Some((func_def.name.to_string(), is_fixture, params));
997 }
998 }
999 }
1000 Stmt::AsyncFunctionDef(func_def) => {
1001 let func_start_line =
1002 self.get_line_from_offset(func_def.range.start().to_usize(), &line_index);
1003 let func_end_line =
1004 self.get_line_from_offset(func_def.range.end().to_usize(), &line_index);
1005
1006 if target_line >= func_start_line && target_line <= func_end_line {
1007 let is_fixture = func_def
1008 .decorator_list
1009 .iter()
1010 .any(decorators::is_fixture_decorator);
1011 let is_test = func_def.name.starts_with("test_");
1012
1013 if is_test || is_fixture {
1014 let params: Vec<String> = func_def
1015 .args
1016 .args
1017 .iter()
1018 .map(|arg| arg.def.arg.to_string())
1019 .collect();
1020
1021 return Some((func_def.name.to_string(), is_fixture, params));
1022 }
1023 }
1024 }
1025 _ => {}
1026 }
1027 }
1028
1029 None
1030 }
1031
1032 pub fn detect_fixture_cycles(&self) -> std::sync::Arc<Vec<super::types::FixtureCycle>> {
1038 use std::sync::Arc;
1039
1040 let current_version = self
1041 .definitions_version
1042 .load(std::sync::atomic::Ordering::SeqCst);
1043
1044 if let Some(cached) = self.cycle_cache.get(&()) {
1046 let (cached_version, cached_cycles) = cached.value();
1047 if *cached_version == current_version {
1048 return Arc::clone(cached_cycles);
1049 }
1050 }
1051
1052 let cycles = Arc::new(self.compute_fixture_cycles());
1054
1055 self.cycle_cache
1057 .insert((), (current_version, Arc::clone(&cycles)));
1058
1059 cycles
1060 }
1061
1062 fn compute_fixture_cycles(&self) -> Vec<super::types::FixtureCycle> {
1065 use super::types::FixtureCycle;
1066 use std::collections::HashMap;
1067
1068 let mut dep_graph: HashMap<String, Vec<String>> = HashMap::new();
1070 let mut fixture_defs: HashMap<String, FixtureDefinition> = HashMap::new();
1071
1072 for entry in self.definitions.iter() {
1073 let fixture_name = entry.key().clone();
1074 if let Some(def) = entry.value().first() {
1075 fixture_defs.insert(fixture_name.clone(), def.clone());
1076 let valid_deps: Vec<String> = def
1078 .dependencies
1079 .iter()
1080 .filter(|d| self.definitions.contains_key(*d))
1081 .cloned()
1082 .collect();
1083 dep_graph.insert(fixture_name, valid_deps);
1084 }
1085 }
1086
1087 let mut cycles = Vec::new();
1088 let mut visited: HashSet<String> = HashSet::new();
1089 let mut seen_cycles: HashSet<String> = HashSet::new(); for start_fixture in dep_graph.keys() {
1093 if visited.contains(start_fixture) {
1094 continue;
1095 }
1096
1097 let mut stack: Vec<(String, usize, Vec<String>)> =
1099 vec![(start_fixture.clone(), 0, vec![])];
1100 let mut rec_stack: HashSet<String> = HashSet::new();
1101
1102 while let Some((current, idx, mut path)) = stack.pop() {
1103 if idx == 0 {
1104 if rec_stack.contains(¤t) {
1106 let cycle_start_idx = path.iter().position(|f| f == ¤t).unwrap_or(0);
1108 let mut cycle_path: Vec<String> = path[cycle_start_idx..].to_vec();
1109 cycle_path.push(current.clone());
1110
1111 let mut cycle_key: Vec<String> =
1113 cycle_path[..cycle_path.len() - 1].to_vec();
1114 cycle_key.sort();
1115 let cycle_key_str = cycle_key.join(",");
1116
1117 if !seen_cycles.contains(&cycle_key_str) {
1118 seen_cycles.insert(cycle_key_str);
1119 if let Some(fixture_def) = fixture_defs.get(¤t) {
1120 cycles.push(FixtureCycle {
1121 cycle_path,
1122 fixture: fixture_def.clone(),
1123 });
1124 }
1125 }
1126 continue;
1127 }
1128
1129 rec_stack.insert(current.clone());
1130 path.push(current.clone());
1131 }
1132
1133 let deps = match dep_graph.get(¤t) {
1135 Some(d) => d,
1136 None => {
1137 rec_stack.remove(¤t);
1138 continue;
1139 }
1140 };
1141
1142 if idx < deps.len() {
1143 stack.push((current.clone(), idx + 1, path.clone()));
1145
1146 let dep = &deps[idx];
1147 if rec_stack.contains(dep) {
1148 let cycle_start_idx = path.iter().position(|f| f == dep).unwrap_or(0);
1150 let mut cycle_path: Vec<String> = path[cycle_start_idx..].to_vec();
1151 cycle_path.push(dep.clone());
1152
1153 let mut cycle_key: Vec<String> =
1154 cycle_path[..cycle_path.len() - 1].to_vec();
1155 cycle_key.sort();
1156 let cycle_key_str = cycle_key.join(",");
1157
1158 if !seen_cycles.contains(&cycle_key_str) {
1159 seen_cycles.insert(cycle_key_str);
1160 if let Some(fixture_def) = fixture_defs.get(dep) {
1161 cycles.push(FixtureCycle {
1162 cycle_path,
1163 fixture: fixture_def.clone(),
1164 });
1165 }
1166 }
1167 } else if !visited.contains(dep) {
1168 stack.push((dep.clone(), 0, path.clone()));
1170 }
1171 } else {
1172 visited.insert(current.clone());
1174 rec_stack.remove(¤t);
1175 }
1176 }
1177 }
1178
1179 cycles
1180 }
1181
1182 pub fn detect_fixture_cycles_in_file(
1186 &self,
1187 file_path: &Path,
1188 ) -> Vec<super::types::FixtureCycle> {
1189 let all_cycles = self.detect_fixture_cycles();
1190 all_cycles
1191 .iter()
1192 .filter(|cycle| cycle.fixture.file_path == file_path)
1193 .cloned()
1194 .collect()
1195 }
1196
1197 pub fn detect_scope_mismatches_in_file(
1203 &self,
1204 file_path: &Path,
1205 ) -> Vec<super::types::ScopeMismatch> {
1206 use super::types::ScopeMismatch;
1207
1208 let mut mismatches = Vec::new();
1209
1210 let Some(fixture_names) = self.file_definitions.get(file_path) else {
1212 return mismatches;
1213 };
1214
1215 for fixture_name in fixture_names.iter() {
1216 let Some(definitions) = self.definitions.get(fixture_name) else {
1218 continue;
1219 };
1220
1221 let Some(fixture_def) = definitions.iter().find(|d| d.file_path == file_path) else {
1223 continue;
1224 };
1225
1226 for dep_name in &fixture_def.dependencies {
1228 if let Some(dep_definitions) = self.definitions.get(dep_name) {
1230 if let Some(dep_def) = dep_definitions.first() {
1233 if fixture_def.scope > dep_def.scope {
1236 mismatches.push(ScopeMismatch {
1237 fixture: fixture_def.clone(),
1238 dependency: dep_def.clone(),
1239 });
1240 }
1241 }
1242 }
1243 }
1244 }
1245
1246 mismatches
1247 }
1248
1249 pub fn resolve_fixture_for_file(
1254 &self,
1255 file_path: &Path,
1256 fixture_name: &str,
1257 ) -> Option<FixtureDefinition> {
1258 let definitions = self.definitions.get(fixture_name)?;
1259
1260 if let Some(def) = definitions.iter().find(|d| d.file_path == file_path) {
1262 return Some(def.clone());
1263 }
1264
1265 let file_path = self.get_canonical_path(file_path.to_path_buf());
1267 let mut best_conftest: Option<&FixtureDefinition> = None;
1268 let mut best_depth = usize::MAX;
1269
1270 for def in definitions.iter() {
1271 if def.is_third_party {
1272 continue;
1273 }
1274 if def.file_path.ends_with("conftest.py") {
1275 if let Some(parent) = def.file_path.parent() {
1276 if file_path.starts_with(parent) {
1277 let depth = parent.components().count();
1278 if depth > best_depth {
1279 best_conftest = Some(def);
1281 best_depth = depth;
1282 } else if best_conftest.is_none() {
1283 best_conftest = Some(def);
1284 best_depth = depth;
1285 }
1286 }
1287 }
1288 }
1289 }
1290
1291 if let Some(def) = best_conftest {
1292 return Some(def.clone());
1293 }
1294
1295 if let Some(def) = definitions
1297 .iter()
1298 .find(|d| d.is_plugin && !d.is_third_party)
1299 {
1300 return Some(def.clone());
1301 }
1302
1303 if let Some(def) = definitions.iter().find(|d| d.is_third_party) {
1305 return Some(def.clone());
1306 }
1307
1308 definitions.first().cloned()
1310 }
1311
1312 pub fn find_containing_function(&self, file_path: &Path, line: usize) -> Option<String> {
1316 let content = self.get_file_content(file_path)?;
1317
1318 let parsed = self.get_parsed_ast(file_path, &content)?;
1320
1321 if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
1322 let line_index = self.get_line_index(file_path, &content);
1324
1325 for stmt in &module.body {
1326 if let Some(name) = self.find_function_containing_line(stmt, line, &line_index) {
1327 return Some(name);
1328 }
1329 }
1330 }
1331
1332 None
1333 }
1334
1335 fn find_function_containing_line(
1337 &self,
1338 stmt: &Stmt,
1339 target_line: usize,
1340 line_index: &[usize],
1341 ) -> Option<String> {
1342 match stmt {
1343 Stmt::FunctionDef(func_def) => {
1344 let start_line =
1345 self.get_line_from_offset(func_def.range.start().to_usize(), line_index);
1346 let end_line =
1347 self.get_line_from_offset(func_def.range.end().to_usize(), line_index);
1348
1349 if target_line >= start_line && target_line <= end_line {
1350 return Some(func_def.name.to_string());
1351 }
1352 }
1353 Stmt::AsyncFunctionDef(func_def) => {
1354 let start_line =
1355 self.get_line_from_offset(func_def.range.start().to_usize(), line_index);
1356 let end_line =
1357 self.get_line_from_offset(func_def.range.end().to_usize(), line_index);
1358
1359 if target_line >= start_line && target_line <= end_line {
1360 return Some(func_def.name.to_string());
1361 }
1362 }
1363 Stmt::ClassDef(class_def) => {
1364 for class_stmt in &class_def.body {
1366 if let Some(name) =
1367 self.find_function_containing_line(class_stmt, target_line, line_index)
1368 {
1369 return Some(name);
1370 }
1371 }
1372 }
1373 _ => {}
1374 }
1375 None
1376 }
1377}