1use super::decorators;
7use super::types::{
8 CompletionContext, FixtureDefinition, FixtureUsage, ParamInsertionInfo, UndeclaredFixture,
9};
10use super::FixtureDatabase;
11use rustpython_parser::ast::{Expr, Ranged, Stmt};
12use rustpython_parser::{parse, Mode};
13use std::collections::HashSet;
14use std::path::Path;
15use tracing::{debug, info};
16
17impl FixtureDatabase {
18 pub fn find_fixture_definition(
20 &self,
21 file_path: &Path,
22 line: u32,
23 character: u32,
24 ) -> Option<FixtureDefinition> {
25 debug!(
26 "find_fixture_definition: file={:?}, line={}, char={}",
27 file_path, line, character
28 );
29
30 let target_line = (line + 1) as usize; let content = self.get_file_content(file_path)?;
33 let line_content = content.lines().nth(target_line.saturating_sub(1))?;
34 debug!("Line content: {}", line_content);
35
36 let word_at_cursor = self.extract_word_at_position(line_content, character as usize)?;
37 debug!("Word at cursor: {:?}", word_at_cursor);
38
39 let current_fixture_def = self.get_fixture_definition_at_line(file_path, target_line);
41
42 if let Some(usages) = self.usages.get(file_path) {
44 for usage in usages.iter() {
45 if usage.line == target_line && usage.name == word_at_cursor {
46 let cursor_pos = character as usize;
47 if cursor_pos >= usage.start_char && cursor_pos < usage.end_char {
48 debug!(
49 "Cursor at {} is within usage range {}-{}: {}",
50 cursor_pos, usage.start_char, usage.end_char, usage.name
51 );
52 info!("Found fixture usage at cursor position: {}", usage.name);
53
54 if let Some(ref current_def) = current_fixture_def {
56 if current_def.name == word_at_cursor {
57 info!(
58 "Self-referencing fixture detected, finding parent definition"
59 );
60 return self.find_closest_definition_excluding(
61 file_path,
62 &usage.name,
63 Some(current_def),
64 );
65 }
66 }
67
68 return self.find_closest_definition(file_path, &usage.name);
69 }
70 }
71 }
72 }
73
74 debug!("Word at cursor '{}' is not a fixture usage", word_at_cursor);
75 None
76 }
77
78 fn get_fixture_definition_at_line(
80 &self,
81 file_path: &Path,
82 line: usize,
83 ) -> Option<FixtureDefinition> {
84 for entry in self.definitions.iter() {
85 for def in entry.value().iter() {
86 if def.file_path == file_path && def.line == line {
87 return Some(def.clone());
88 }
89 }
90 }
91 None
92 }
93
94 pub fn get_definition_at_line(
96 &self,
97 file_path: &Path,
98 line: usize,
99 fixture_name: &str,
100 ) -> Option<FixtureDefinition> {
101 if let Some(definitions) = self.definitions.get(fixture_name) {
102 for def in definitions.iter() {
103 if def.file_path == file_path && def.line == line {
104 return Some(def.clone());
105 }
106 }
107 }
108 None
109 }
110
111 pub(crate) fn find_closest_definition(
113 &self,
114 file_path: &Path,
115 fixture_name: &str,
116 ) -> Option<FixtureDefinition> {
117 let definitions = self.definitions.get(fixture_name)?;
118
119 debug!(
121 "Checking for fixture {} in same file: {:?}",
122 fixture_name, file_path
123 );
124
125 if let Some(last_def) = definitions
126 .iter()
127 .filter(|def| def.file_path == file_path)
128 .max_by_key(|def| def.line)
129 {
130 info!(
131 "Found fixture {} in same file at line {} (using last definition)",
132 fixture_name, last_def.line
133 );
134 return Some(last_def.clone());
135 }
136
137 let mut current_dir = file_path.parent()?;
139
140 debug!(
141 "Searching for fixture {} in conftest.py files starting from {:?}",
142 fixture_name, current_dir
143 );
144 loop {
145 let conftest_path = current_dir.join("conftest.py");
146 debug!(" Checking conftest.py at: {:?}", conftest_path);
147
148 for def in definitions.iter() {
149 if def.file_path == conftest_path {
150 info!(
151 "Found fixture {} in conftest.py: {:?}",
152 fixture_name, conftest_path
153 );
154 return Some(def.clone());
155 }
156 }
157
158 match current_dir.parent() {
159 Some(parent) => current_dir = parent,
160 None => break,
161 }
162 }
163
164 debug!(
166 "No fixture {} found in conftest hierarchy, checking for third-party fixtures",
167 fixture_name
168 );
169 for def in definitions.iter() {
170 if def.is_third_party {
171 info!(
172 "Found third-party fixture {} in site-packages: {:?}",
173 fixture_name, def.file_path
174 );
175 return Some(def.clone());
176 }
177 }
178
179 debug!(
180 "No fixture {} found in scope for {:?}",
181 fixture_name, file_path
182 );
183 None
184 }
185
186 pub(crate) fn find_closest_definition_excluding(
188 &self,
189 file_path: &Path,
190 fixture_name: &str,
191 exclude: Option<&FixtureDefinition>,
192 ) -> Option<FixtureDefinition> {
193 let definitions = self.definitions.get(fixture_name)?;
194
195 debug!(
197 "Checking for fixture {} in same file: {:?} (excluding: {:?})",
198 fixture_name, file_path, exclude
199 );
200
201 if let Some(last_def) = definitions
202 .iter()
203 .filter(|def| {
204 if def.file_path != file_path {
205 return false;
206 }
207 if let Some(excluded) = exclude {
208 if def == &excluded {
209 debug!("Skipping excluded definition at line {}", def.line);
210 return false;
211 }
212 }
213 true
214 })
215 .max_by_key(|def| def.line)
216 {
217 info!(
218 "Found fixture {} in same file at line {} (excluding specified)",
219 fixture_name, last_def.line
220 );
221 return Some(last_def.clone());
222 }
223
224 let mut current_dir = file_path.parent()?;
226
227 debug!(
228 "Searching for fixture {} in conftest.py files starting from {:?}",
229 fixture_name, current_dir
230 );
231 loop {
232 let conftest_path = current_dir.join("conftest.py");
233 debug!(" Checking conftest.py at: {:?}", conftest_path);
234
235 for def in definitions.iter() {
236 if def.file_path == conftest_path {
237 if let Some(excluded) = exclude {
238 if def == excluded {
239 debug!("Skipping excluded definition at line {}", def.line);
240 continue;
241 }
242 }
243 info!(
244 "Found fixture {} in conftest.py: {:?}",
245 fixture_name, conftest_path
246 );
247 return Some(def.clone());
248 }
249 }
250
251 match current_dir.parent() {
252 Some(parent) => current_dir = parent,
253 None => break,
254 }
255 }
256
257 debug!(
259 "No fixture {} found in conftest hierarchy (excluding specified), checking third-party",
260 fixture_name
261 );
262 for def in definitions.iter() {
263 if let Some(excluded) = exclude {
264 if def == excluded {
265 continue;
266 }
267 }
268 if def.is_third_party {
269 info!(
270 "Found third-party fixture {} in site-packages: {:?}",
271 fixture_name, def.file_path
272 );
273 return Some(def.clone());
274 }
275 }
276
277 debug!(
278 "No fixture {} found in scope for {:?} (excluding specified)",
279 fixture_name, file_path
280 );
281 None
282 }
283
284 pub fn find_fixture_at_position(
286 &self,
287 file_path: &Path,
288 line: u32,
289 character: u32,
290 ) -> Option<String> {
291 let target_line = (line + 1) as usize;
292
293 debug!(
294 "find_fixture_at_position: file={:?}, line={}, char={}",
295 file_path, target_line, character
296 );
297
298 let content = self.get_file_content(file_path)?;
299 let line_content = content.lines().nth(target_line.saturating_sub(1))?;
300 debug!("Line content: {}", line_content);
301
302 let word_at_cursor = self.extract_word_at_position(line_content, character as usize);
303 debug!("Word at cursor: {:?}", word_at_cursor);
304
305 if let Some(usages) = self.usages.get(file_path) {
307 for usage in usages.iter() {
308 if usage.line == target_line {
309 let cursor_pos = character as usize;
310 if cursor_pos >= usage.start_char && cursor_pos < usage.end_char {
311 debug!(
312 "Cursor at {} is within usage range {}-{}: {}",
313 cursor_pos, usage.start_char, usage.end_char, usage.name
314 );
315 info!("Found fixture usage at cursor position: {}", usage.name);
316 return Some(usage.name.clone());
317 }
318 }
319 }
320 }
321
322 for entry in self.definitions.iter() {
324 for def in entry.value().iter() {
325 if def.file_path == file_path && def.line == target_line {
326 if let Some(ref word) = word_at_cursor {
327 if word == &def.name {
328 info!(
329 "Found fixture definition name at cursor position: {}",
330 def.name
331 );
332 return Some(def.name.clone());
333 }
334 }
335 }
336 }
337 }
338
339 debug!("No fixture found at cursor position");
340 None
341 }
342
343 pub fn extract_word_at_position(&self, line: &str, character: usize) -> Option<String> {
345 super::string_utils::extract_word_at_position(line, character)
346 }
347
348 pub fn find_fixture_references(&self, fixture_name: &str) -> Vec<FixtureUsage> {
350 info!("Finding all references for fixture: {}", fixture_name);
351
352 let mut all_references = Vec::new();
353
354 for entry in self.usages.iter() {
355 let file_path = entry.key();
356 let usages = entry.value();
357
358 for usage in usages.iter() {
359 if usage.name == fixture_name {
360 debug!(
361 "Found reference to {} in {:?} at line {}",
362 fixture_name, file_path, usage.line
363 );
364 all_references.push(usage.clone());
365 }
366 }
367 }
368
369 info!(
370 "Found {} total references for fixture: {}",
371 all_references.len(),
372 fixture_name
373 );
374 all_references
375 }
376
377 pub fn find_references_for_definition(
379 &self,
380 definition: &FixtureDefinition,
381 ) -> Vec<FixtureUsage> {
382 info!(
383 "Finding references for specific definition: {} at {:?}:{}",
384 definition.name, definition.file_path, definition.line
385 );
386
387 let mut matching_references = Vec::new();
388
389 for entry in self.usages.iter() {
390 let file_path = entry.key();
391 let usages = entry.value();
392
393 for usage in usages.iter() {
394 if usage.name == definition.name {
395 let fixture_def_at_line =
396 self.get_fixture_definition_at_line(file_path, usage.line);
397
398 let resolved_def = if let Some(ref current_def) = fixture_def_at_line {
399 if current_def.name == usage.name {
400 debug!(
401 "Usage at {:?}:{} is self-referencing, excluding definition at line {}",
402 file_path, usage.line, current_def.line
403 );
404 self.find_closest_definition_excluding(
405 file_path,
406 &usage.name,
407 Some(current_def),
408 )
409 } else {
410 self.find_closest_definition(file_path, &usage.name)
411 }
412 } else {
413 self.find_closest_definition(file_path, &usage.name)
414 };
415
416 if let Some(resolved_def) = resolved_def {
417 if resolved_def == *definition {
418 debug!(
419 "Usage at {:?}:{} resolves to our definition",
420 file_path, usage.line
421 );
422 matching_references.push(usage.clone());
423 } else {
424 debug!(
425 "Usage at {:?}:{} resolves to different definition at {:?}:{}",
426 file_path, usage.line, resolved_def.file_path, resolved_def.line
427 );
428 }
429 }
430 }
431 }
432 }
433
434 info!(
435 "Found {} references that resolve to this specific definition",
436 matching_references.len()
437 );
438 matching_references
439 }
440
441 pub fn get_undeclared_fixtures(&self, file_path: &Path) -> Vec<UndeclaredFixture> {
443 self.undeclared_fixtures
444 .get(file_path)
445 .map(|entry| entry.value().clone())
446 .unwrap_or_default()
447 }
448
449 pub fn get_available_fixtures(&self, file_path: &Path) -> Vec<FixtureDefinition> {
451 let mut available_fixtures = Vec::new();
452 let mut seen_names = HashSet::new();
453
454 for entry in self.definitions.iter() {
456 let fixture_name = entry.key();
457 for def in entry.value().iter() {
458 if def.file_path == file_path && !seen_names.contains(fixture_name.as_str()) {
459 available_fixtures.push(def.clone());
460 seen_names.insert(fixture_name.clone());
461 }
462 }
463 }
464
465 if let Some(mut current_dir) = file_path.parent() {
467 loop {
468 let conftest_path = current_dir.join("conftest.py");
469
470 for entry in self.definitions.iter() {
471 let fixture_name = entry.key();
472 for def in entry.value().iter() {
473 if def.file_path == conftest_path
474 && !seen_names.contains(fixture_name.as_str())
475 {
476 available_fixtures.push(def.clone());
477 seen_names.insert(fixture_name.clone());
478 }
479 }
480 }
481
482 match current_dir.parent() {
483 Some(parent) => current_dir = parent,
484 None => break,
485 }
486 }
487 }
488
489 for entry in self.definitions.iter() {
491 let fixture_name = entry.key();
492 for def in entry.value().iter() {
493 if def.is_third_party && !seen_names.contains(fixture_name.as_str()) {
494 available_fixtures.push(def.clone());
495 seen_names.insert(fixture_name.clone());
496 }
497 }
498 }
499
500 available_fixtures.sort_by(|a, b| a.name.cmp(&b.name));
501 available_fixtures
502 }
503
504 pub fn get_completion_context(
506 &self,
507 file_path: &Path,
508 line: u32,
509 character: u32,
510 ) -> Option<CompletionContext> {
511 let content = self.get_file_content(file_path)?;
512 let target_line = (line + 1) as usize;
513
514 let parsed = parse(&content, Mode::Module, "").ok()?;
515
516 if let rustpython_parser::ast::Mod::Module(module) = parsed {
517 if let Some(ctx) = Self::check_decorator_context(&module.body, &content, target_line) {
519 return Some(ctx);
520 }
521
522 return self.get_function_completion_context(
524 &module.body,
525 &content,
526 target_line,
527 character as usize,
528 );
529 }
530
531 None
532 }
533
534 fn check_decorator_context(
536 stmts: &[Stmt],
537 content: &str,
538 target_line: usize,
539 ) -> Option<CompletionContext> {
540 for stmt in stmts {
541 let decorator_list = match stmt {
542 Stmt::FunctionDef(f) => &f.decorator_list,
543 Stmt::AsyncFunctionDef(f) => &f.decorator_list,
544 Stmt::ClassDef(c) => &c.decorator_list,
545 _ => continue,
546 };
547
548 for decorator in decorator_list {
549 let dec_start_line = content[..decorator.range().start().to_usize()]
550 .matches('\n')
551 .count()
552 + 1;
553 let dec_end_line = content[..decorator.range().end().to_usize()]
554 .matches('\n')
555 .count()
556 + 1;
557
558 if target_line >= dec_start_line && target_line <= dec_end_line {
559 if decorators::is_usefixtures_decorator(decorator) {
560 return Some(CompletionContext::UsefixuturesDecorator);
561 }
562 if decorators::is_parametrize_decorator(decorator) {
563 return Some(CompletionContext::ParametrizeIndirect);
564 }
565 }
566 }
567
568 if let Stmt::ClassDef(class_def) = stmt {
570 if let Some(ctx) =
571 Self::check_decorator_context(&class_def.body, content, target_line)
572 {
573 return Some(ctx);
574 }
575 }
576 }
577
578 None
579 }
580
581 fn get_function_completion_context(
583 &self,
584 stmts: &[Stmt],
585 content: &str,
586 target_line: usize,
587 target_char: usize,
588 ) -> Option<CompletionContext> {
589 for stmt in stmts {
590 match stmt {
591 Stmt::FunctionDef(func_def) => {
592 if let Some(ctx) = self.get_func_context(
593 &func_def.name,
594 &func_def.decorator_list,
595 &func_def.args,
596 func_def.range,
597 content,
598 target_line,
599 target_char,
600 ) {
601 return Some(ctx);
602 }
603 }
604 Stmt::AsyncFunctionDef(func_def) => {
605 if let Some(ctx) = self.get_func_context(
606 &func_def.name,
607 &func_def.decorator_list,
608 &func_def.args,
609 func_def.range,
610 content,
611 target_line,
612 target_char,
613 ) {
614 return Some(ctx);
615 }
616 }
617 Stmt::ClassDef(class_def) => {
618 if let Some(ctx) = self.get_function_completion_context(
619 &class_def.body,
620 content,
621 target_line,
622 target_char,
623 ) {
624 return Some(ctx);
625 }
626 }
627 _ => {}
628 }
629 }
630
631 None
632 }
633
634 #[allow(clippy::too_many_arguments)]
636 fn get_func_context(
637 &self,
638 func_name: &rustpython_parser::ast::Identifier,
639 decorator_list: &[Expr],
640 args: &rustpython_parser::ast::Arguments,
641 range: rustpython_parser::text_size::TextRange,
642 content: &str,
643 target_line: usize,
644 _target_char: usize,
645 ) -> Option<CompletionContext> {
646 let func_start_line = content[..range.start().to_usize()].matches('\n').count() + 1;
647 let func_end_line = content[..range.end().to_usize()].matches('\n').count() + 1;
648
649 if target_line < func_start_line || target_line > func_end_line {
650 return None;
651 }
652
653 let is_fixture = decorator_list.iter().any(decorators::is_fixture_decorator);
654 let is_test = func_name.as_str().starts_with("test_");
655
656 if !is_test && !is_fixture {
657 return None;
658 }
659
660 let params: Vec<String> = FixtureDatabase::all_args(args)
662 .map(|arg| arg.def.arg.to_string())
663 .collect();
664
665 let lines: Vec<&str> = content.lines().collect();
667
668 let mut sig_end_line = func_start_line;
669 for (i, line) in lines
670 .iter()
671 .enumerate()
672 .skip(func_start_line.saturating_sub(1))
673 {
674 if line.contains("):") {
675 sig_end_line = i + 1;
676 break;
677 }
678 if i + 1 > func_start_line + 10 {
679 break;
680 }
681 }
682
683 let in_signature = target_line <= sig_end_line;
684
685 let context = if in_signature {
686 CompletionContext::FunctionSignature {
687 function_name: func_name.to_string(),
688 function_line: func_start_line,
689 is_fixture,
690 declared_params: params,
691 }
692 } else {
693 CompletionContext::FunctionBody {
694 function_name: func_name.to_string(),
695 function_line: func_start_line,
696 is_fixture,
697 declared_params: params,
698 }
699 };
700
701 Some(context)
702 }
703
704 pub fn get_function_param_insertion_info(
706 &self,
707 file_path: &Path,
708 function_line: usize,
709 ) -> Option<ParamInsertionInfo> {
710 let content = self.get_file_content(file_path)?;
711 let lines: Vec<&str> = content.lines().collect();
712
713 for i in (function_line.saturating_sub(1))..lines.len().min(function_line + 10) {
714 let line = lines[i];
715 if let Some(paren_pos) = line.find("):") {
716 let has_params = if let Some(open_pos) = line.find('(') {
717 if open_pos < paren_pos {
718 let params_section = &line[open_pos + 1..paren_pos];
719 !params_section.trim().is_empty()
720 } else {
721 true
722 }
723 } else {
724 let before_close = &line[..paren_pos];
725 if !before_close.trim().is_empty() {
726 true
727 } else {
728 let mut found_params = false;
729 for prev_line in lines.iter().take(i).skip(function_line.saturating_sub(1))
730 {
731 if prev_line.contains('(') {
732 if let Some(open_pos) = prev_line.find('(') {
733 let after_open = &prev_line[open_pos + 1..];
734 if !after_open.trim().is_empty() {
735 found_params = true;
736 break;
737 }
738 }
739 } else if !prev_line.trim().is_empty() {
740 found_params = true;
741 break;
742 }
743 }
744 found_params
745 }
746 };
747
748 return Some(ParamInsertionInfo {
749 line: i + 1,
750 char_pos: paren_pos,
751 needs_comma: has_params,
752 });
753 }
754 }
755
756 None
757 }
758
759 #[allow(dead_code)] #[allow(dead_code)] pub fn is_inside_function(
764 &self,
765 file_path: &Path,
766 line: u32,
767 character: u32,
768 ) -> Option<(String, bool, Vec<String>)> {
769 let content = self.get_file_content(file_path)?;
771
772 let target_line = (line + 1) as usize; let parsed = parse(&content, Mode::Module, "").ok()?;
776
777 if let rustpython_parser::ast::Mod::Module(module) = parsed {
778 return self.find_enclosing_function(
779 &module.body,
780 &content,
781 target_line,
782 character as usize,
783 );
784 }
785
786 None
787 }
788
789 #[allow(dead_code)]
790 fn find_enclosing_function(
791 &self,
792 stmts: &[Stmt],
793 content: &str,
794 target_line: usize,
795 _target_char: usize,
796 ) -> Option<(String, bool, Vec<String>)> {
797 let line_index = Self::build_line_index(content);
798
799 for stmt in stmts {
800 match stmt {
801 Stmt::FunctionDef(func_def) => {
802 let func_start_line =
803 self.get_line_from_offset(func_def.range.start().to_usize(), &line_index);
804 let func_end_line =
805 self.get_line_from_offset(func_def.range.end().to_usize(), &line_index);
806
807 if target_line >= func_start_line && target_line <= func_end_line {
809 let is_fixture = func_def
810 .decorator_list
811 .iter()
812 .any(decorators::is_fixture_decorator);
813 let is_test = func_def.name.starts_with("test_");
814
815 if is_test || is_fixture {
817 let params: Vec<String> = func_def
818 .args
819 .args
820 .iter()
821 .map(|arg| arg.def.arg.to_string())
822 .collect();
823
824 return Some((func_def.name.to_string(), is_fixture, params));
825 }
826 }
827 }
828 Stmt::AsyncFunctionDef(func_def) => {
829 let func_start_line =
830 self.get_line_from_offset(func_def.range.start().to_usize(), &line_index);
831 let func_end_line =
832 self.get_line_from_offset(func_def.range.end().to_usize(), &line_index);
833
834 if target_line >= func_start_line && target_line <= func_end_line {
835 let is_fixture = func_def
836 .decorator_list
837 .iter()
838 .any(decorators::is_fixture_decorator);
839 let is_test = func_def.name.starts_with("test_");
840
841 if is_test || is_fixture {
842 let params: Vec<String> = func_def
843 .args
844 .args
845 .iter()
846 .map(|arg| arg.def.arg.to_string())
847 .collect();
848
849 return Some((func_def.name.to_string(), is_fixture, params));
850 }
851 }
852 }
853 _ => {}
854 }
855 }
856
857 None
858 }
859}