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 get_definition_at_line(
95 &self,
96 file_path: &Path,
97 line: usize,
98 fixture_name: &str,
99 ) -> Option<FixtureDefinition> {
100 if let Some(definitions) = self.definitions.get(fixture_name) {
101 for def in definitions.iter() {
102 if def.file_path == file_path && def.line == line {
103 return Some(def.clone());
104 }
105 }
106 }
107 None
108 }
109
110 pub(crate) fn find_closest_definition(
112 &self,
113 file_path: &Path,
114 fixture_name: &str,
115 ) -> Option<FixtureDefinition> {
116 self.find_closest_definition_with_filter(file_path, fixture_name, |_| true)
117 }
118
119 pub(crate) fn find_closest_definition_excluding(
121 &self,
122 file_path: &Path,
123 fixture_name: &str,
124 exclude: Option<&FixtureDefinition>,
125 ) -> Option<FixtureDefinition> {
126 self.find_closest_definition_with_filter(file_path, fixture_name, |def| {
127 if let Some(excluded) = exclude {
128 def != excluded
129 } else {
130 true
131 }
132 })
133 }
134
135 fn find_closest_definition_with_filter<F>(
141 &self,
142 file_path: &Path,
143 fixture_name: &str,
144 filter: F,
145 ) -> Option<FixtureDefinition>
146 where
147 F: Fn(&FixtureDefinition) -> bool,
148 {
149 let definitions = self.definitions.get(fixture_name)?;
150
151 debug!(
153 "Checking for fixture {} in same file: {:?}",
154 fixture_name, file_path
155 );
156
157 if let Some(last_def) = definitions
158 .iter()
159 .filter(|def| def.file_path == file_path && filter(def))
160 .max_by_key(|def| def.line)
161 {
162 info!(
163 "Found fixture {} in same file at line {}",
164 fixture_name, last_def.line
165 );
166 return Some(last_def.clone());
167 }
168
169 let mut current_dir = file_path.parent()?;
171
172 debug!(
173 "Searching for fixture {} in conftest.py files starting from {:?}",
174 fixture_name, current_dir
175 );
176 loop {
177 let conftest_path = current_dir.join("conftest.py");
178 debug!(" Checking conftest.py at: {:?}", conftest_path);
179
180 for def in definitions.iter() {
181 if def.file_path == conftest_path && filter(def) {
182 info!(
183 "Found fixture {} in conftest.py: {:?}",
184 fixture_name, conftest_path
185 );
186 return Some(def.clone());
187 }
188 }
189
190 match current_dir.parent() {
191 Some(parent) => current_dir = parent,
192 None => break,
193 }
194 }
195
196 debug!(
198 "No fixture {} found in conftest hierarchy, checking third-party",
199 fixture_name
200 );
201 for def in definitions.iter() {
202 if def.is_third_party && filter(def) {
203 info!(
204 "Found third-party fixture {} in site-packages: {:?}",
205 fixture_name, def.file_path
206 );
207 return Some(def.clone());
208 }
209 }
210
211 debug!(
212 "No fixture {} found in scope for {:?}",
213 fixture_name, file_path
214 );
215 None
216 }
217
218 pub fn find_fixture_at_position(
220 &self,
221 file_path: &Path,
222 line: u32,
223 character: u32,
224 ) -> Option<String> {
225 let target_line = (line + 1) as usize;
226
227 debug!(
228 "find_fixture_at_position: file={:?}, line={}, char={}",
229 file_path, target_line, character
230 );
231
232 let content = self.get_file_content(file_path)?;
233 let line_content = content.lines().nth(target_line.saturating_sub(1))?;
234 debug!("Line content: {}", line_content);
235
236 let word_at_cursor = self.extract_word_at_position(line_content, character as usize);
237 debug!("Word at cursor: {:?}", word_at_cursor);
238
239 if let Some(usages) = self.usages.get(file_path) {
241 for usage in usages.iter() {
242 if usage.line == target_line {
243 let cursor_pos = character as usize;
244 if cursor_pos >= usage.start_char && cursor_pos < usage.end_char {
245 debug!(
246 "Cursor at {} is within usage range {}-{}: {}",
247 cursor_pos, usage.start_char, usage.end_char, usage.name
248 );
249 info!("Found fixture usage at cursor position: {}", usage.name);
250 return Some(usage.name.clone());
251 }
252 }
253 }
254 }
255
256 for entry in self.definitions.iter() {
258 for def in entry.value().iter() {
259 if def.file_path == file_path && def.line == target_line {
260 if let Some(ref word) = word_at_cursor {
261 if word == &def.name {
262 info!(
263 "Found fixture definition name at cursor position: {}",
264 def.name
265 );
266 return Some(def.name.clone());
267 }
268 }
269 }
270 }
271 }
272
273 debug!("No fixture found at cursor position");
274 None
275 }
276
277 pub fn extract_word_at_position(&self, line: &str, character: usize) -> Option<String> {
279 super::string_utils::extract_word_at_position(line, character)
280 }
281
282 pub fn find_fixture_references(&self, fixture_name: &str) -> Vec<FixtureUsage> {
284 info!("Finding all references for fixture: {}", fixture_name);
285
286 let mut all_references = Vec::new();
287
288 for entry in self.usages.iter() {
289 let file_path = entry.key();
290 let usages = entry.value();
291
292 for usage in usages.iter() {
293 if usage.name == fixture_name {
294 debug!(
295 "Found reference to {} in {:?} at line {}",
296 fixture_name, file_path, usage.line
297 );
298 all_references.push(usage.clone());
299 }
300 }
301 }
302
303 info!(
304 "Found {} total references for fixture: {}",
305 all_references.len(),
306 fixture_name
307 );
308 all_references
309 }
310
311 pub fn find_references_for_definition(
313 &self,
314 definition: &FixtureDefinition,
315 ) -> Vec<FixtureUsage> {
316 info!(
317 "Finding references for specific definition: {} at {:?}:{}",
318 definition.name, definition.file_path, definition.line
319 );
320
321 let mut matching_references = Vec::new();
322
323 for entry in self.usages.iter() {
324 let file_path = entry.key();
325 let usages = entry.value();
326
327 for usage in usages.iter() {
328 if usage.name == definition.name {
329 let fixture_def_at_line =
330 self.get_fixture_definition_at_line(file_path, usage.line);
331
332 let resolved_def = if let Some(ref current_def) = fixture_def_at_line {
333 if current_def.name == usage.name {
334 debug!(
335 "Usage at {:?}:{} is self-referencing, excluding definition at line {}",
336 file_path, usage.line, current_def.line
337 );
338 self.find_closest_definition_excluding(
339 file_path,
340 &usage.name,
341 Some(current_def),
342 )
343 } else {
344 self.find_closest_definition(file_path, &usage.name)
345 }
346 } else {
347 self.find_closest_definition(file_path, &usage.name)
348 };
349
350 if let Some(resolved_def) = resolved_def {
351 if resolved_def == *definition {
352 debug!(
353 "Usage at {:?}:{} resolves to our definition",
354 file_path, usage.line
355 );
356 matching_references.push(usage.clone());
357 } else {
358 debug!(
359 "Usage at {:?}:{} resolves to different definition at {:?}:{}",
360 file_path, usage.line, resolved_def.file_path, resolved_def.line
361 );
362 }
363 }
364 }
365 }
366 }
367
368 info!(
369 "Found {} references that resolve to this specific definition",
370 matching_references.len()
371 );
372 matching_references
373 }
374
375 pub fn get_undeclared_fixtures(&self, file_path: &Path) -> Vec<UndeclaredFixture> {
377 self.undeclared_fixtures
378 .get(file_path)
379 .map(|entry| entry.value().clone())
380 .unwrap_or_default()
381 }
382
383 pub fn get_available_fixtures(&self, file_path: &Path) -> Vec<FixtureDefinition> {
385 let mut available_fixtures = Vec::new();
386 let mut seen_names = HashSet::new();
387
388 for entry in self.definitions.iter() {
390 let fixture_name = entry.key();
391 for def in entry.value().iter() {
392 if def.file_path == file_path && !seen_names.contains(fixture_name.as_str()) {
393 available_fixtures.push(def.clone());
394 seen_names.insert(fixture_name.clone());
395 }
396 }
397 }
398
399 if let Some(mut current_dir) = file_path.parent() {
401 loop {
402 let conftest_path = current_dir.join("conftest.py");
403
404 for entry in self.definitions.iter() {
405 let fixture_name = entry.key();
406 for def in entry.value().iter() {
407 if def.file_path == conftest_path
408 && !seen_names.contains(fixture_name.as_str())
409 {
410 available_fixtures.push(def.clone());
411 seen_names.insert(fixture_name.clone());
412 }
413 }
414 }
415
416 match current_dir.parent() {
417 Some(parent) => current_dir = parent,
418 None => break,
419 }
420 }
421 }
422
423 for entry in self.definitions.iter() {
425 let fixture_name = entry.key();
426 for def in entry.value().iter() {
427 if def.is_third_party && !seen_names.contains(fixture_name.as_str()) {
428 available_fixtures.push(def.clone());
429 seen_names.insert(fixture_name.clone());
430 }
431 }
432 }
433
434 available_fixtures.sort_by(|a, b| a.name.cmp(&b.name));
435 available_fixtures
436 }
437
438 pub fn get_completion_context(
440 &self,
441 file_path: &Path,
442 line: u32,
443 character: u32,
444 ) -> Option<CompletionContext> {
445 let content = self.get_file_content(file_path)?;
446 let target_line = (line + 1) as usize;
447 let line_index = self.get_line_index(file_path, &content);
448
449 let parsed = self.get_parsed_ast(file_path, &content)?;
450
451 if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
452 if let Some(ctx) =
454 self.check_decorator_context(&module.body, &content, target_line, &line_index)
455 {
456 return Some(ctx);
457 }
458
459 return self.get_function_completion_context(
461 &module.body,
462 &content,
463 target_line,
464 character as usize,
465 &line_index,
466 );
467 }
468
469 None
470 }
471
472 fn check_decorator_context(
474 &self,
475 stmts: &[Stmt],
476 _content: &str,
477 target_line: usize,
478 line_index: &[usize],
479 ) -> Option<CompletionContext> {
480 for stmt in stmts {
481 let decorator_list = match stmt {
482 Stmt::FunctionDef(f) => &f.decorator_list,
483 Stmt::AsyncFunctionDef(f) => &f.decorator_list,
484 Stmt::ClassDef(c) => &c.decorator_list,
485 _ => continue,
486 };
487
488 for decorator in decorator_list {
489 let dec_start_line =
490 self.get_line_from_offset(decorator.range().start().to_usize(), line_index);
491 let dec_end_line =
492 self.get_line_from_offset(decorator.range().end().to_usize(), line_index);
493
494 if target_line >= dec_start_line && target_line <= dec_end_line {
495 if decorators::is_usefixtures_decorator(decorator) {
496 return Some(CompletionContext::UsefixuturesDecorator);
497 }
498 if decorators::is_parametrize_decorator(decorator) {
499 return Some(CompletionContext::ParametrizeIndirect);
500 }
501 }
502 }
503
504 if let Stmt::ClassDef(class_def) = stmt {
506 if let Some(ctx) =
507 self.check_decorator_context(&class_def.body, _content, target_line, line_index)
508 {
509 return Some(ctx);
510 }
511 }
512 }
513
514 None
515 }
516
517 fn get_function_completion_context(
519 &self,
520 stmts: &[Stmt],
521 content: &str,
522 target_line: usize,
523 target_char: usize,
524 line_index: &[usize],
525 ) -> Option<CompletionContext> {
526 for stmt in stmts {
527 match stmt {
528 Stmt::FunctionDef(func_def) => {
529 if let Some(ctx) = self.get_func_context(
530 &func_def.name,
531 &func_def.decorator_list,
532 &func_def.args,
533 func_def.range,
534 content,
535 target_line,
536 target_char,
537 line_index,
538 ) {
539 return Some(ctx);
540 }
541 }
542 Stmt::AsyncFunctionDef(func_def) => {
543 if let Some(ctx) = self.get_func_context(
544 &func_def.name,
545 &func_def.decorator_list,
546 &func_def.args,
547 func_def.range,
548 content,
549 target_line,
550 target_char,
551 line_index,
552 ) {
553 return Some(ctx);
554 }
555 }
556 Stmt::ClassDef(class_def) => {
557 if let Some(ctx) = self.get_function_completion_context(
558 &class_def.body,
559 content,
560 target_line,
561 target_char,
562 line_index,
563 ) {
564 return Some(ctx);
565 }
566 }
567 _ => {}
568 }
569 }
570
571 None
572 }
573
574 #[allow(clippy::too_many_arguments)]
576 fn get_func_context(
577 &self,
578 func_name: &rustpython_parser::ast::Identifier,
579 decorator_list: &[Expr],
580 args: &rustpython_parser::ast::Arguments,
581 range: rustpython_parser::text_size::TextRange,
582 content: &str,
583 target_line: usize,
584 _target_char: usize,
585 line_index: &[usize],
586 ) -> Option<CompletionContext> {
587 let func_start_line = self.get_line_from_offset(range.start().to_usize(), line_index);
588 let func_end_line = self.get_line_from_offset(range.end().to_usize(), line_index);
589
590 if target_line < func_start_line || target_line > func_end_line {
591 return None;
592 }
593
594 let is_fixture = decorator_list.iter().any(decorators::is_fixture_decorator);
595 let is_test = func_name.as_str().starts_with("test_");
596
597 if !is_test && !is_fixture {
598 return None;
599 }
600
601 let params: Vec<String> = FixtureDatabase::all_args(args)
603 .map(|arg| arg.def.arg.to_string())
604 .collect();
605
606 let lines: Vec<&str> = content.lines().collect();
608
609 let mut sig_end_line = func_start_line;
610 for (i, line) in lines
611 .iter()
612 .enumerate()
613 .skip(func_start_line.saturating_sub(1))
614 {
615 if line.contains("):") {
616 sig_end_line = i + 1;
617 break;
618 }
619 if i + 1 > func_start_line + 10 {
620 break;
621 }
622 }
623
624 let in_signature = target_line <= sig_end_line;
625
626 let context = if in_signature {
627 CompletionContext::FunctionSignature {
628 function_name: func_name.to_string(),
629 function_line: func_start_line,
630 is_fixture,
631 declared_params: params,
632 }
633 } else {
634 CompletionContext::FunctionBody {
635 function_name: func_name.to_string(),
636 function_line: func_start_line,
637 is_fixture,
638 declared_params: params,
639 }
640 };
641
642 Some(context)
643 }
644
645 pub fn get_function_param_insertion_info(
647 &self,
648 file_path: &Path,
649 function_line: usize,
650 ) -> Option<ParamInsertionInfo> {
651 let content = self.get_file_content(file_path)?;
652 let lines: Vec<&str> = content.lines().collect();
653
654 for i in (function_line.saturating_sub(1))..lines.len().min(function_line + 10) {
655 let line = lines[i];
656 if let Some(paren_pos) = line.find("):") {
657 let has_params = if let Some(open_pos) = line.find('(') {
658 if open_pos < paren_pos {
659 let params_section = &line[open_pos + 1..paren_pos];
660 !params_section.trim().is_empty()
661 } else {
662 true
663 }
664 } else {
665 let before_close = &line[..paren_pos];
666 if !before_close.trim().is_empty() {
667 true
668 } else {
669 let mut found_params = false;
670 for prev_line in lines.iter().take(i).skip(function_line.saturating_sub(1))
671 {
672 if prev_line.contains('(') {
673 if let Some(open_pos) = prev_line.find('(') {
674 let after_open = &prev_line[open_pos + 1..];
675 if !after_open.trim().is_empty() {
676 found_params = true;
677 break;
678 }
679 }
680 } else if !prev_line.trim().is_empty() {
681 found_params = true;
682 break;
683 }
684 }
685 found_params
686 }
687 };
688
689 return Some(ParamInsertionInfo {
690 line: i + 1,
691 char_pos: paren_pos,
692 needs_comma: has_params,
693 });
694 }
695 }
696
697 None
698 }
699
700 #[allow(dead_code)] #[allow(dead_code)] pub fn is_inside_function(
705 &self,
706 file_path: &Path,
707 line: u32,
708 character: u32,
709 ) -> Option<(String, bool, Vec<String>)> {
710 let content = self.get_file_content(file_path)?;
712
713 let target_line = (line + 1) as usize; let parsed = self.get_parsed_ast(file_path, &content)?;
717
718 if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
719 return self.find_enclosing_function(
720 &module.body,
721 &content,
722 target_line,
723 character as usize,
724 );
725 }
726
727 None
728 }
729
730 #[allow(dead_code)]
731 fn find_enclosing_function(
732 &self,
733 stmts: &[Stmt],
734 content: &str,
735 target_line: usize,
736 _target_char: usize,
737 ) -> Option<(String, bool, Vec<String>)> {
738 let line_index = Self::build_line_index(content);
739
740 for stmt in stmts {
741 match stmt {
742 Stmt::FunctionDef(func_def) => {
743 let func_start_line =
744 self.get_line_from_offset(func_def.range.start().to_usize(), &line_index);
745 let func_end_line =
746 self.get_line_from_offset(func_def.range.end().to_usize(), &line_index);
747
748 if target_line >= func_start_line && target_line <= func_end_line {
750 let is_fixture = func_def
751 .decorator_list
752 .iter()
753 .any(decorators::is_fixture_decorator);
754 let is_test = func_def.name.starts_with("test_");
755
756 if is_test || is_fixture {
758 let params: Vec<String> = func_def
759 .args
760 .args
761 .iter()
762 .map(|arg| arg.def.arg.to_string())
763 .collect();
764
765 return Some((func_def.name.to_string(), is_fixture, params));
766 }
767 }
768 }
769 Stmt::AsyncFunctionDef(func_def) => {
770 let func_start_line =
771 self.get_line_from_offset(func_def.range.start().to_usize(), &line_index);
772 let func_end_line =
773 self.get_line_from_offset(func_def.range.end().to_usize(), &line_index);
774
775 if target_line >= func_start_line && target_line <= func_end_line {
776 let is_fixture = func_def
777 .decorator_list
778 .iter()
779 .any(decorators::is_fixture_decorator);
780 let is_test = func_def.name.starts_with("test_");
781
782 if is_test || is_fixture {
783 let params: Vec<String> = func_def
784 .args
785 .args
786 .iter()
787 .map(|arg| arg.def.arg.to_string())
788 .collect();
789
790 return Some((func_def.name.to_string(), is_fixture, params));
791 }
792 }
793 }
794 _ => {}
795 }
796 }
797
798 None
799 }
800}