1use super::decorators;
7use super::types::{
8 CompletionContext, FixtureDefinition, FixtureScope, FixtureUsage, ParamInsertionInfo,
9 UndeclaredFixture,
10};
11use super::FixtureDatabase;
12use rustpython_parser::ast::{Expr, Ranged, Stmt};
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 find_fixture_or_definition_at_position(
99 &self,
100 file_path: &Path,
101 line: u32,
102 character: u32,
103 ) -> Option<FixtureDefinition> {
104 if let Some(def) = self.find_fixture_definition(file_path, line, character) {
106 return Some(def);
107 }
108
109 let target_line = (line + 1) as usize; let content = self.get_file_content(file_path)?;
112 let line_content = content.lines().nth(target_line.saturating_sub(1))?;
113 let word_at_cursor = self.extract_word_at_position(line_content, character as usize)?;
114
115 if let Some(definitions) = self.definitions.get(&word_at_cursor) {
117 for def in definitions.iter() {
118 if def.file_path == file_path && def.line == target_line {
119 if character as usize >= def.start_char && (character as usize) < def.end_char {
121 return Some(def.clone());
122 }
123 }
124 }
125 }
126
127 None
128 }
129
130 pub fn get_definition_at_line(
132 &self,
133 file_path: &Path,
134 line: usize,
135 fixture_name: &str,
136 ) -> Option<FixtureDefinition> {
137 if let Some(definitions) = self.definitions.get(fixture_name) {
138 for def in definitions.iter() {
139 if def.file_path == file_path && def.line == line {
140 return Some(def.clone());
141 }
142 }
143 }
144 None
145 }
146
147 pub(crate) fn find_closest_definition(
149 &self,
150 file_path: &Path,
151 fixture_name: &str,
152 ) -> Option<FixtureDefinition> {
153 self.find_closest_definition_with_filter(file_path, fixture_name, |_| true)
154 }
155
156 pub(crate) fn find_closest_definition_excluding(
158 &self,
159 file_path: &Path,
160 fixture_name: &str,
161 exclude: Option<&FixtureDefinition>,
162 ) -> Option<FixtureDefinition> {
163 self.find_closest_definition_with_filter(file_path, fixture_name, |def| {
164 if let Some(excluded) = exclude {
165 def != excluded
166 } else {
167 true
168 }
169 })
170 }
171
172 fn find_closest_definition_with_filter<F>(
178 &self,
179 file_path: &Path,
180 fixture_name: &str,
181 filter: F,
182 ) -> Option<FixtureDefinition>
183 where
184 F: Fn(&FixtureDefinition) -> bool,
185 {
186 let definitions = self.definitions.get(fixture_name)?;
187
188 debug!(
190 "Checking for fixture {} in same file: {:?}",
191 fixture_name, file_path
192 );
193
194 if let Some(last_def) = definitions
195 .iter()
196 .filter(|def| def.file_path == file_path && filter(def))
197 .max_by_key(|def| def.line)
198 {
199 info!(
200 "Found fixture {} in same file at line {}",
201 fixture_name, last_def.line
202 );
203 return Some(last_def.clone());
204 }
205
206 let mut current_dir = file_path.parent()?;
208
209 debug!(
210 "Searching for fixture {} in conftest.py files starting from {:?}",
211 fixture_name, current_dir
212 );
213 loop {
214 let conftest_path = current_dir.join("conftest.py");
215 debug!(" Checking conftest.py at: {:?}", conftest_path);
216
217 for def in definitions.iter() {
219 if def.file_path == conftest_path && filter(def) {
220 info!(
221 "Found fixture {} in conftest.py: {:?}",
222 fixture_name, conftest_path
223 );
224 return Some(def.clone());
225 }
226 }
227
228 let conftest_in_cache = self.file_cache.contains_key(&conftest_path);
231 if (conftest_path.exists() || conftest_in_cache)
232 && self.is_fixture_imported_in_file(fixture_name, &conftest_path)
233 {
234 debug!(
237 "Fixture {} is imported in conftest.py: {:?}",
238 fixture_name, conftest_path
239 );
240 if let Some(def) = definitions.iter().find(|def| filter(def)) {
242 info!(
243 "Found imported fixture {} via conftest.py: {:?} (original: {:?})",
244 fixture_name, conftest_path, def.file_path
245 );
246 return Some(def.clone());
247 }
248 }
249
250 match current_dir.parent() {
251 Some(parent) => current_dir = parent,
252 None => break,
253 }
254 }
255
256 debug!(
260 "No fixture {} found in conftest hierarchy, checking plugins",
261 fixture_name
262 );
263 for def in definitions.iter() {
264 if def.is_plugin && !def.is_third_party && filter(def) {
265 info!(
266 "Found plugin fixture {} via pytest11 entry point: {:?}",
267 fixture_name, def.file_path
268 );
269 return Some(def.clone());
270 }
271 }
272
273 debug!(
275 "No fixture {} found in plugins, checking third-party",
276 fixture_name
277 );
278 for def in definitions.iter() {
279 if def.is_third_party && filter(def) {
280 info!(
281 "Found third-party fixture {} in site-packages: {:?}",
282 fixture_name, def.file_path
283 );
284 return Some(def.clone());
285 }
286 }
287
288 debug!(
289 "No fixture {} found in scope for {:?}",
290 fixture_name, file_path
291 );
292 None
293 }
294
295 pub fn find_fixture_at_position(
297 &self,
298 file_path: &Path,
299 line: u32,
300 character: u32,
301 ) -> Option<String> {
302 let target_line = (line + 1) as usize;
303
304 debug!(
305 "find_fixture_at_position: file={:?}, line={}, char={}",
306 file_path, target_line, character
307 );
308
309 let content = self.get_file_content(file_path)?;
310 let line_content = content.lines().nth(target_line.saturating_sub(1))?;
311 debug!("Line content: {}", line_content);
312
313 let word_at_cursor = self.extract_word_at_position(line_content, character as usize);
314 debug!("Word at cursor: {:?}", word_at_cursor);
315
316 if let Some(usages) = self.usages.get(file_path) {
318 for usage in usages.iter() {
319 if usage.line == target_line {
320 let cursor_pos = character as usize;
321 if cursor_pos >= usage.start_char && cursor_pos < usage.end_char {
322 debug!(
323 "Cursor at {} is within usage range {}-{}: {}",
324 cursor_pos, usage.start_char, usage.end_char, usage.name
325 );
326 info!("Found fixture usage at cursor position: {}", usage.name);
327 return Some(usage.name.clone());
328 }
329 }
330 }
331 }
332
333 for entry in self.definitions.iter() {
335 for def in entry.value().iter() {
336 if def.file_path == file_path && def.line == target_line {
337 if let Some(ref word) = word_at_cursor {
338 if word == &def.name {
339 info!(
340 "Found fixture definition name at cursor position: {}",
341 def.name
342 );
343 return Some(def.name.clone());
344 }
345 }
346 }
347 }
348 }
349
350 debug!("No fixture found at cursor position");
351 None
352 }
353
354 pub fn extract_word_at_position(&self, line: &str, character: usize) -> Option<String> {
356 super::string_utils::extract_word_at_position(line, character)
357 }
358
359 pub fn find_fixture_references(&self, fixture_name: &str) -> Vec<FixtureUsage> {
361 info!("Finding all references for fixture: {}", fixture_name);
362
363 let mut all_references = Vec::new();
364
365 for entry in self.usages.iter() {
366 let file_path = entry.key();
367 let usages = entry.value();
368
369 for usage in usages.iter() {
370 if usage.name == fixture_name {
371 debug!(
372 "Found reference to {} in {:?} at line {}",
373 fixture_name, file_path, usage.line
374 );
375 all_references.push(usage.clone());
376 }
377 }
378 }
379
380 info!(
381 "Found {} total references for fixture: {}",
382 all_references.len(),
383 fixture_name
384 );
385 all_references
386 }
387
388 pub fn find_references_for_definition(
392 &self,
393 definition: &FixtureDefinition,
394 ) -> Vec<FixtureUsage> {
395 info!(
396 "Finding references for specific definition: {} at {:?}:{}",
397 definition.name, definition.file_path, definition.line
398 );
399
400 let mut matching_references = Vec::new();
401
402 let Some(usages_for_fixture) = self.usage_by_fixture.get(&definition.name) else {
404 info!("No references found for fixture: {}", definition.name);
405 return matching_references;
406 };
407
408 for (file_path, usage) in usages_for_fixture.iter() {
409 let fixture_def_at_line = self.get_fixture_definition_at_line(file_path, usage.line);
410
411 let resolved_def = if let Some(ref current_def) = fixture_def_at_line {
412 if current_def.name == usage.name {
413 debug!(
414 "Usage at {:?}:{} is self-referencing, excluding definition at line {}",
415 file_path, usage.line, current_def.line
416 );
417 self.find_closest_definition_excluding(
418 file_path,
419 &usage.name,
420 Some(current_def),
421 )
422 } else {
423 self.find_closest_definition(file_path, &usage.name)
424 }
425 } else {
426 self.find_closest_definition(file_path, &usage.name)
427 };
428
429 if let Some(resolved_def) = resolved_def {
430 if resolved_def == *definition {
431 debug!(
432 "Usage at {:?}:{} resolves to our definition",
433 file_path, usage.line
434 );
435 matching_references.push(usage.clone());
436 } else {
437 debug!(
438 "Usage at {:?}:{} resolves to different definition at {:?}:{}",
439 file_path, usage.line, resolved_def.file_path, resolved_def.line
440 );
441 }
442 }
443 }
444
445 info!(
446 "Found {} references that resolve to this specific definition",
447 matching_references.len()
448 );
449 matching_references
450 }
451
452 pub fn get_undeclared_fixtures(&self, file_path: &Path) -> Vec<UndeclaredFixture> {
454 self.undeclared_fixtures
455 .get(file_path)
456 .map(|entry| entry.value().clone())
457 .unwrap_or_default()
458 }
459
460 pub fn get_available_fixtures(&self, file_path: &Path) -> Vec<FixtureDefinition> {
464 use std::sync::Arc;
465
466 let file_path = self.get_canonical_path(file_path.to_path_buf());
468
469 let current_version = self
471 .definitions_version
472 .load(std::sync::atomic::Ordering::SeqCst);
473
474 if let Some(cached) = self.available_fixtures_cache.get(&file_path) {
475 let (cached_version, cached_fixtures) = cached.value();
476 if *cached_version == current_version {
477 return cached_fixtures.as_ref().clone();
479 }
480 }
481
482 let available_fixtures = self.compute_available_fixtures(&file_path);
484
485 self.available_fixtures_cache.insert(
487 file_path,
488 (current_version, Arc::new(available_fixtures.clone())),
489 );
490
491 available_fixtures
492 }
493
494 fn compute_available_fixtures(&self, file_path: &Path) -> Vec<FixtureDefinition> {
496 let mut available_fixtures = Vec::new();
497 let mut seen_names = HashSet::new();
498
499 for entry in self.definitions.iter() {
501 let fixture_name = entry.key();
502 for def in entry.value().iter() {
503 if def.file_path == file_path && !seen_names.contains(fixture_name.as_str()) {
504 available_fixtures.push(def.clone());
505 seen_names.insert(fixture_name.clone());
506 }
507 }
508 }
509
510 if let Some(mut current_dir) = file_path.parent() {
512 loop {
513 let conftest_path = current_dir.join("conftest.py");
514
515 for entry in self.definitions.iter() {
517 let fixture_name = entry.key();
518 for def in entry.value().iter() {
519 if def.file_path == conftest_path
520 && !seen_names.contains(fixture_name.as_str())
521 {
522 available_fixtures.push(def.clone());
523 seen_names.insert(fixture_name.clone());
524 }
525 }
526 }
527
528 if self.file_cache.contains_key(&conftest_path) {
530 let mut visited = HashSet::new();
531 let imported_fixtures =
532 self.get_imported_fixtures(&conftest_path, &mut visited);
533 for fixture_name in imported_fixtures {
534 if !seen_names.contains(&fixture_name) {
535 if let Some(definitions) = self.definitions.get(&fixture_name) {
537 if let Some(def) = definitions.first() {
538 available_fixtures.push(def.clone());
539 seen_names.insert(fixture_name);
540 }
541 }
542 }
543 }
544 }
545
546 match current_dir.parent() {
547 Some(parent) => current_dir = parent,
548 None => break,
549 }
550 }
551 }
552
553 for entry in self.definitions.iter() {
555 let fixture_name = entry.key();
556 for def in entry.value().iter() {
557 if def.is_plugin
558 && !def.is_third_party
559 && !seen_names.contains(fixture_name.as_str())
560 {
561 available_fixtures.push(def.clone());
562 seen_names.insert(fixture_name.clone());
563 }
564 }
565 }
566
567 for entry in self.definitions.iter() {
569 let fixture_name = entry.key();
570 for def in entry.value().iter() {
571 if def.is_third_party && !seen_names.contains(fixture_name.as_str()) {
572 available_fixtures.push(def.clone());
573 seen_names.insert(fixture_name.clone());
574 }
575 }
576 }
577
578 available_fixtures.sort_by(|a, b| a.name.cmp(&b.name));
579 available_fixtures
580 }
581
582 pub fn get_completion_context(
584 &self,
585 file_path: &Path,
586 line: u32,
587 character: u32,
588 ) -> Option<CompletionContext> {
589 let content = self.get_file_content(file_path)?;
590 let target_line = (line + 1) as usize;
591
592 let parsed = self.get_parsed_ast(file_path, &content);
594
595 if let Some(parsed) = parsed {
596 let line_index = self.get_line_index(file_path, &content);
597
598 if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
599 if let Some(ctx) =
601 self.check_decorator_context(&module.body, &content, target_line, &line_index)
602 {
603 return Some(ctx);
604 }
605
606 if let Some(ctx) = self.get_function_completion_context(
608 &module.body,
609 &content,
610 target_line,
611 character as usize,
612 &line_index,
613 ) {
614 return Some(ctx);
615 }
616 }
617 }
618
619 self.get_completion_context_from_text(&content, target_line)
621 }
622
623 fn has_fixture_decorator_above(lines: &[&str], def_line_idx: usize) -> bool {
629 if def_line_idx == 0 {
630 return false;
631 }
632 let mut i = def_line_idx - 1;
633 loop {
634 let trimmed = lines[i].trim();
635 if trimmed.is_empty() {
636 if i == 0 {
638 break;
639 }
640 i -= 1;
641 continue;
642 }
643 if trimmed.starts_with('@') {
644 if trimmed.contains("pytest.fixture") || trimmed.starts_with("@fixture") {
646 return true;
647 }
648 if i == 0 {
650 break;
651 }
652 i -= 1;
653 continue;
654 }
655 break;
657 }
658 false
659 }
660
661 fn extract_fixture_scope_from_text(
667 lines: &[&str],
668 def_line_idx: usize,
669 ) -> Option<FixtureScope> {
670 if def_line_idx == 0 {
671 return None;
672 }
673
674 let mut i = def_line_idx - 1;
676 loop {
677 let trimmed = lines[i].trim();
678 if trimmed.is_empty() {
679 if i == 0 {
680 break;
681 }
682 i -= 1;
683 continue;
684 }
685 if trimmed.starts_with('@') {
686 for pattern in &["scope=\"", "scope='"] {
688 if let Some(pos) = trimmed.find(pattern) {
689 let start = pos + pattern.len();
690 let quote_char = if pattern.ends_with('"') { '"' } else { '\'' };
691 if let Some(end) = trimmed[start..].find(quote_char) {
692 let scope_str = &trimmed[start..start + end];
693 return FixtureScope::parse(scope_str);
694 }
695 }
696 }
697 if i == 0 {
698 break;
699 }
700 i -= 1;
701 continue;
702 }
703 break;
704 }
705
706 None
707 }
708
709 fn get_usefixtures_context_from_text(
718 lines: &[&str],
719 cursor_idx: usize,
720 ) -> Option<CompletionContext> {
721 let scan_limit = cursor_idx.saturating_sub(10);
723
724 let mut i = cursor_idx;
725 loop {
726 let line = lines[i];
727 if let Some(pos) = line.find("usefixtures(") {
728 let mut depth: i32 = 0;
731
732 for ch in line[pos..].chars() {
734 if ch == '(' {
735 depth += 1;
736 }
737 if ch == ')' {
738 depth -= 1;
739 }
740 }
741
742 if i < cursor_idx {
745 for line in &lines[(i + 1)..=cursor_idx] {
746 for ch in line.chars() {
747 if ch == '(' {
748 depth += 1;
749 }
750 if ch == ')' {
751 depth -= 1;
752 }
753 }
754 }
755 }
756
757 if depth > 0 {
759 return Some(CompletionContext::UsefixturesDecorator);
760 }
761
762 if i == cursor_idx && depth == 0 {
766 if let Some(close_pos) = line[pos..].rfind(')') {
769 let abs_close = pos + close_pos;
770 let open_pos = pos + line[pos..].find('(').unwrap_or(0);
775 if abs_close == open_pos + 1 {
776 return Some(CompletionContext::UsefixturesDecorator);
778 }
779 return None;
781 }
782 return Some(CompletionContext::UsefixturesDecorator);
784 }
785 }
786
787 if i == 0 || i <= scan_limit {
788 break;
789 }
790 i -= 1;
791 }
792
793 None
794 }
795
796 fn get_completion_context_from_text(
802 &self,
803 content: &str,
804 target_line: usize,
805 ) -> Option<CompletionContext> {
806 let mut lines: Vec<&str> = content.lines().collect();
807
808 if content.ends_with('\n') {
811 lines.push("");
812 }
813
814 if target_line == 0 || target_line > lines.len() {
815 return None;
816 }
817
818 let cursor_idx = target_line - 1; if let Some(ctx) = Self::get_usefixtures_context_from_text(&lines, cursor_idx) {
822 return Some(ctx);
823 }
824
825 let mut def_line_idx = None;
832 let scan_limit = cursor_idx.saturating_sub(50);
833
834 let mut i = cursor_idx;
835 loop {
836 let trimmed = lines[i].trim();
837 if trimmed.starts_with("def ") || trimmed.starts_with("async def ") {
838 def_line_idx = Some(i);
839 break;
840 }
841 if i == 0 || i <= scan_limit {
842 break;
843 }
844 i -= 1;
845 }
846
847 let def_line_idx = def_line_idx?;
848 let def_line = lines[def_line_idx].trim();
849
850 let name_start = if def_line.starts_with("async def ") {
852 "async def ".len()
853 } else {
854 "def ".len()
855 };
856 let remaining = &def_line[name_start..];
857 let func_name: String = remaining
858 .chars()
859 .take_while(|c| c.is_alphanumeric() || *c == '_')
860 .collect();
861
862 if func_name.is_empty() {
863 return None;
864 }
865
866 let is_test = func_name.starts_with("test_");
868 let is_fixture = Self::has_fixture_decorator_above(&lines, def_line_idx);
869
870 if !is_test && !is_fixture {
872 return None;
873 }
874
875 let mut paren_depth: i32 = 0;
880 let mut cursor_inside_parens = false;
881 let mut found_open = false;
882 let mut signature_closed = false; for (line_idx_offset, line) in lines[def_line_idx..=cursor_idx].iter().enumerate() {
885 let current_line_idx = def_line_idx + line_idx_offset;
886 let is_cursor_line = current_line_idx == cursor_idx;
887
888 for ch in line.chars() {
889 if ch == '(' {
890 paren_depth += 1;
891 if paren_depth == 1 {
892 found_open = true;
893 }
894 } else if ch == ')' {
895 paren_depth -= 1;
896 if paren_depth == 0 && found_open {
897 if !is_cursor_line {
899 signature_closed = true;
901 }
902 }
906 }
907 }
908
909 if is_cursor_line && found_open && paren_depth > 0 {
911 cursor_inside_parens = true;
912 }
913 }
914
915 if signature_closed && !cursor_inside_parens {
925 return None;
926 }
927
928 let mut declared_params = Vec::new();
935 if found_open {
936 let mut param_text = String::new();
937 let mut past_open = false;
938 let mut past_close = false;
939 for line in &lines[def_line_idx..=cursor_idx] {
940 for ch in line.chars() {
941 if past_close {
942 continue;
944 } else if past_open {
945 if ch == ')' {
946 past_close = true;
947 } else {
948 param_text.push(ch);
949 }
950 } else if ch == '(' {
951 past_open = true;
952 }
953 }
954 if past_open && !past_close {
955 param_text.push(' ');
956 }
957 }
958 for param in param_text.split(',') {
959 let name: String = param
960 .trim()
961 .chars()
962 .take_while(|c| c.is_alphanumeric() || *c == '_')
963 .collect();
964 if !name.is_empty() {
965 declared_params.push(name);
966 }
967 }
968 }
969
970 let fixture_scope = if is_fixture {
972 let scope = Self::extract_fixture_scope_from_text(&lines, def_line_idx)
973 .unwrap_or(FixtureScope::Function);
974 Some(scope)
975 } else {
976 None
977 };
978
979 Some(CompletionContext::FunctionSignature {
980 function_name: func_name,
981 function_line: def_line_idx + 1, is_fixture,
983 declared_params,
984 fixture_scope,
985 })
986 }
987
988 fn check_decorator_context(
991 &self,
992 stmts: &[Stmt],
993 _content: &str,
994 target_line: usize,
995 line_index: &[usize],
996 ) -> Option<CompletionContext> {
997 for stmt in stmts {
998 let decorator_list = match stmt {
1000 Stmt::FunctionDef(f) => Some(f.decorator_list.as_slice()),
1001 Stmt::AsyncFunctionDef(f) => Some(f.decorator_list.as_slice()),
1002 Stmt::ClassDef(c) => Some(c.decorator_list.as_slice()),
1003 _ => None,
1004 };
1005
1006 if let Some(decorator_list) = decorator_list {
1007 for decorator in decorator_list {
1008 let dec_start_line =
1009 self.get_line_from_offset(decorator.range().start().to_usize(), line_index);
1010 let dec_end_line =
1011 self.get_line_from_offset(decorator.range().end().to_usize(), line_index);
1012
1013 if target_line >= dec_start_line && target_line <= dec_end_line {
1014 if decorators::is_usefixtures_decorator(decorator) {
1015 return Some(CompletionContext::UsefixturesDecorator);
1016 }
1017 if decorators::is_parametrize_decorator(decorator) {
1018 return Some(CompletionContext::ParametrizeIndirect);
1019 }
1020 }
1021 }
1022 }
1023
1024 let pytestmark_value: Option<&Expr> = match stmt {
1026 Stmt::Assign(assign) => {
1027 let is_pytestmark = assign
1028 .targets
1029 .iter()
1030 .any(|t| matches!(t, Expr::Name(n) if n.id.as_str() == "pytestmark"));
1031 if is_pytestmark {
1032 Some(assign.value.as_ref())
1033 } else {
1034 None
1035 }
1036 }
1037 Stmt::AnnAssign(ann_assign) => {
1038 let is_pytestmark = matches!(
1039 ann_assign.target.as_ref(),
1040 Expr::Name(n) if n.id.as_str() == "pytestmark"
1041 );
1042 if is_pytestmark {
1043 ann_assign.value.as_ref().map(|v| v.as_ref())
1044 } else {
1045 None
1046 }
1047 }
1048 _ => None,
1049 };
1050
1051 if let Some(value) = pytestmark_value {
1052 let stmt_start =
1053 self.get_line_from_offset(stmt.range().start().to_usize(), line_index);
1054 let stmt_end = self.get_line_from_offset(stmt.range().end().to_usize(), line_index);
1055
1056 if target_line >= stmt_start
1057 && target_line <= stmt_end
1058 && self.cursor_inside_usefixtures_call(value, target_line, line_index)
1059 {
1060 return Some(CompletionContext::UsefixturesDecorator);
1061 }
1062 }
1063
1064 if let Stmt::ClassDef(class_def) = stmt {
1066 if let Some(ctx) =
1067 self.check_decorator_context(&class_def.body, _content, target_line, line_index)
1068 {
1069 return Some(ctx);
1070 }
1071 }
1072 }
1073
1074 None
1075 }
1076
1077 fn cursor_inside_usefixtures_call(
1080 &self,
1081 expr: &Expr,
1082 target_line: usize,
1083 line_index: &[usize],
1084 ) -> bool {
1085 match expr {
1086 Expr::Call(call) => {
1087 if decorators::is_usefixtures_decorator(&call.func) {
1088 let call_start =
1089 self.get_line_from_offset(expr.range().start().to_usize(), line_index);
1090 let call_end =
1091 self.get_line_from_offset(expr.range().end().to_usize(), line_index);
1092 return target_line >= call_start && target_line <= call_end;
1093 }
1094 false
1095 }
1096 Expr::List(list) => list
1097 .elts
1098 .iter()
1099 .any(|e| self.cursor_inside_usefixtures_call(e, target_line, line_index)),
1100 Expr::Tuple(tuple) => tuple
1101 .elts
1102 .iter()
1103 .any(|e| self.cursor_inside_usefixtures_call(e, target_line, line_index)),
1104 _ => false,
1105 }
1106 }
1107
1108 fn get_function_completion_context(
1110 &self,
1111 stmts: &[Stmt],
1112 content: &str,
1113 target_line: usize,
1114 target_char: usize,
1115 line_index: &[usize],
1116 ) -> Option<CompletionContext> {
1117 for stmt in stmts {
1118 match stmt {
1119 Stmt::FunctionDef(func_def) => {
1120 if let Some(ctx) = self.get_func_context(
1121 &func_def.name,
1122 &func_def.decorator_list,
1123 &func_def.args,
1124 &func_def.returns,
1125 &func_def.body,
1126 func_def.range,
1127 content,
1128 target_line,
1129 target_char,
1130 line_index,
1131 ) {
1132 return Some(ctx);
1133 }
1134 }
1135 Stmt::AsyncFunctionDef(func_def) => {
1136 if let Some(ctx) = self.get_func_context(
1137 &func_def.name,
1138 &func_def.decorator_list,
1139 &func_def.args,
1140 &func_def.returns,
1141 &func_def.body,
1142 func_def.range,
1143 content,
1144 target_line,
1145 target_char,
1146 line_index,
1147 ) {
1148 return Some(ctx);
1149 }
1150 }
1151 Stmt::ClassDef(class_def) => {
1152 if let Some(ctx) = self.get_function_completion_context(
1153 &class_def.body,
1154 content,
1155 target_line,
1156 target_char,
1157 line_index,
1158 ) {
1159 return Some(ctx);
1160 }
1161 }
1162 _ => {}
1163 }
1164 }
1165
1166 None
1167 }
1168
1169 fn find_signature_end_line(
1175 &self,
1176 func_start_line: usize,
1177 args: &rustpython_parser::ast::Arguments,
1178 returns: &Option<Box<Expr>>,
1179 body: &[Stmt],
1180 content: &str,
1181 line_index: &[usize],
1182 ) -> usize {
1183 let mut last_sig_offset: Option<usize> = None;
1185
1186 if let Some(ret) = returns {
1188 last_sig_offset = Some(ret.range().end().to_usize());
1189 }
1190
1191 let all_arg_ends = args
1193 .args
1194 .iter()
1195 .chain(args.posonlyargs.iter())
1196 .chain(args.kwonlyargs.iter())
1197 .map(|a| a.def.range.end().to_usize())
1198 .chain(args.vararg.as_ref().map(|a| a.range.end().to_usize()))
1199 .chain(args.kwarg.as_ref().map(|a| a.range.end().to_usize()));
1200
1201 if let Some(max_arg_end) = all_arg_ends.max() {
1202 last_sig_offset =
1203 Some(last_sig_offset.map_or(max_arg_end, |prev| prev.max(max_arg_end)));
1204 }
1205
1206 let last_sig_line = last_sig_offset
1208 .map(|offset| self.get_line_from_offset(offset, line_index))
1209 .unwrap_or(func_start_line);
1210
1211 let first_body_line = body
1213 .first()
1214 .map(|stmt| self.get_line_from_offset(stmt.range().start().to_usize(), line_index));
1215
1216 let lines: Vec<&str> = content.lines().collect();
1218 let scan_end = first_body_line
1219 .unwrap_or(last_sig_line + 10)
1220 .min(last_sig_line + 10)
1221 .min(lines.len());
1222 let scan_start = last_sig_line.saturating_sub(1);
1223
1224 for (i, line) in lines
1225 .iter()
1226 .enumerate()
1227 .skip(scan_start)
1228 .take(scan_end.saturating_sub(scan_start))
1229 {
1230 let trimmed = line.trim();
1231 if trimmed.ends_with(':') {
1232 return i + 1; }
1234 }
1235
1236 if let Some(body_line) = first_body_line {
1238 return body_line.saturating_sub(1).max(func_start_line);
1239 }
1240
1241 func_start_line
1243 }
1244
1245 #[allow(clippy::too_many_arguments)]
1247 fn get_func_context(
1248 &self,
1249 func_name: &rustpython_parser::ast::Identifier,
1250 decorator_list: &[Expr],
1251 args: &rustpython_parser::ast::Arguments,
1252 returns: &Option<Box<Expr>>,
1253 body: &[Stmt],
1254 range: rustpython_parser::text_size::TextRange,
1255 content: &str,
1256 target_line: usize,
1257 _target_char: usize,
1258 line_index: &[usize],
1259 ) -> Option<CompletionContext> {
1260 let func_start_line = self.get_line_from_offset(range.start().to_usize(), line_index);
1261 let func_end_line = self.get_line_from_offset(range.end().to_usize(), line_index);
1262
1263 if target_line < func_start_line || target_line > func_end_line {
1264 return None;
1265 }
1266
1267 let is_fixture = decorator_list.iter().any(decorators::is_fixture_decorator);
1268 let is_test = func_name.as_str().starts_with("test_");
1269
1270 if !is_test && !is_fixture {
1271 return None;
1272 }
1273
1274 let fixture_scope = if is_fixture {
1276 let scope = decorator_list
1277 .iter()
1278 .find_map(decorators::extract_fixture_scope)
1279 .unwrap_or(super::types::FixtureScope::Function);
1280 Some(scope)
1281 } else {
1282 None
1283 };
1284
1285 let params: Vec<String> = FixtureDatabase::all_args(args)
1287 .map(|arg| arg.def.arg.to_string())
1288 .collect();
1289
1290 let sig_end_line =
1292 self.find_signature_end_line(func_start_line, args, returns, body, content, line_index);
1293
1294 let in_signature = target_line <= sig_end_line;
1295
1296 let context = if in_signature {
1297 CompletionContext::FunctionSignature {
1298 function_name: func_name.to_string(),
1299 function_line: func_start_line,
1300 is_fixture,
1301 declared_params: params,
1302 fixture_scope,
1303 }
1304 } else {
1305 CompletionContext::FunctionBody {
1306 function_name: func_name.to_string(),
1307 function_line: func_start_line,
1308 is_fixture,
1309 declared_params: params,
1310 fixture_scope,
1311 }
1312 };
1313
1314 Some(context)
1315 }
1316
1317 pub fn get_function_param_insertion_info(
1319 &self,
1320 file_path: &Path,
1321 function_line: usize,
1322 ) -> Option<ParamInsertionInfo> {
1323 let content = self.get_file_content(file_path)?;
1324 let lines: Vec<&str> = content.lines().collect();
1325
1326 for i in (function_line.saturating_sub(1))..lines.len().min(function_line + 10) {
1327 let line = lines[i];
1328 if let Some(paren_pos) = line.find("):") {
1329 let has_params = if let Some(open_pos) = line.find('(') {
1330 if open_pos < paren_pos {
1331 let params_section = &line[open_pos + 1..paren_pos];
1332 !params_section.trim().is_empty()
1333 } else {
1334 true
1335 }
1336 } else {
1337 let before_close = &line[..paren_pos];
1338 if !before_close.trim().is_empty() {
1339 true
1340 } else {
1341 let mut found_params = false;
1342 for prev_line in lines.iter().take(i).skip(function_line.saturating_sub(1))
1343 {
1344 if prev_line.contains('(') {
1345 if let Some(open_pos) = prev_line.find('(') {
1346 let after_open = &prev_line[open_pos + 1..];
1347 if !after_open.trim().is_empty() {
1348 found_params = true;
1349 break;
1350 }
1351 }
1352 } else if !prev_line.trim().is_empty() {
1353 found_params = true;
1354 break;
1355 }
1356 }
1357 found_params
1358 }
1359 };
1360
1361 return Some(ParamInsertionInfo {
1362 line: i + 1,
1363 char_pos: paren_pos,
1364 needs_comma: has_params,
1365 });
1366 }
1367 }
1368
1369 None
1370 }
1371
1372 #[allow(dead_code)] #[allow(dead_code)] pub fn is_inside_function(
1377 &self,
1378 file_path: &Path,
1379 line: u32,
1380 character: u32,
1381 ) -> Option<(String, bool, Vec<String>)> {
1382 let content = self.get_file_content(file_path)?;
1384
1385 let target_line = (line + 1) as usize; let parsed = self.get_parsed_ast(file_path, &content)?;
1389
1390 if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
1391 return self.find_enclosing_function(
1392 &module.body,
1393 &content,
1394 target_line,
1395 character as usize,
1396 );
1397 }
1398
1399 None
1400 }
1401
1402 #[allow(dead_code)]
1403 fn find_enclosing_function(
1404 &self,
1405 stmts: &[Stmt],
1406 content: &str,
1407 target_line: usize,
1408 _target_char: usize,
1409 ) -> Option<(String, bool, Vec<String>)> {
1410 let line_index = Self::build_line_index(content);
1411
1412 for stmt in stmts {
1413 match stmt {
1414 Stmt::FunctionDef(func_def) => {
1415 let func_start_line =
1416 self.get_line_from_offset(func_def.range.start().to_usize(), &line_index);
1417 let func_end_line =
1418 self.get_line_from_offset(func_def.range.end().to_usize(), &line_index);
1419
1420 if target_line >= func_start_line && target_line <= func_end_line {
1422 let is_fixture = func_def
1423 .decorator_list
1424 .iter()
1425 .any(decorators::is_fixture_decorator);
1426 let is_test = func_def.name.starts_with("test_");
1427
1428 if is_test || is_fixture {
1430 let params: Vec<String> = func_def
1431 .args
1432 .args
1433 .iter()
1434 .map(|arg| arg.def.arg.to_string())
1435 .collect();
1436
1437 return Some((func_def.name.to_string(), is_fixture, params));
1438 }
1439 }
1440 }
1441 Stmt::AsyncFunctionDef(func_def) => {
1442 let func_start_line =
1443 self.get_line_from_offset(func_def.range.start().to_usize(), &line_index);
1444 let func_end_line =
1445 self.get_line_from_offset(func_def.range.end().to_usize(), &line_index);
1446
1447 if target_line >= func_start_line && target_line <= func_end_line {
1448 let is_fixture = func_def
1449 .decorator_list
1450 .iter()
1451 .any(decorators::is_fixture_decorator);
1452 let is_test = func_def.name.starts_with("test_");
1453
1454 if is_test || is_fixture {
1455 let params: Vec<String> = func_def
1456 .args
1457 .args
1458 .iter()
1459 .map(|arg| arg.def.arg.to_string())
1460 .collect();
1461
1462 return Some((func_def.name.to_string(), is_fixture, params));
1463 }
1464 }
1465 }
1466 _ => {}
1467 }
1468 }
1469
1470 None
1471 }
1472
1473 pub fn detect_fixture_cycles(&self) -> std::sync::Arc<Vec<super::types::FixtureCycle>> {
1479 use std::sync::Arc;
1480
1481 let current_version = self
1482 .definitions_version
1483 .load(std::sync::atomic::Ordering::SeqCst);
1484
1485 if let Some(cached) = self.cycle_cache.get(&()) {
1487 let (cached_version, cached_cycles) = cached.value();
1488 if *cached_version == current_version {
1489 return Arc::clone(cached_cycles);
1490 }
1491 }
1492
1493 let cycles = Arc::new(self.compute_fixture_cycles());
1495
1496 self.cycle_cache
1498 .insert((), (current_version, Arc::clone(&cycles)));
1499
1500 cycles
1501 }
1502
1503 fn compute_fixture_cycles(&self) -> Vec<super::types::FixtureCycle> {
1506 use super::types::FixtureCycle;
1507 use std::collections::HashMap;
1508
1509 let mut dep_graph: HashMap<String, Vec<String>> = HashMap::new();
1511 let mut fixture_defs: HashMap<String, FixtureDefinition> = HashMap::new();
1512
1513 for entry in self.definitions.iter() {
1514 let fixture_name = entry.key().clone();
1515 if let Some(def) = entry.value().first() {
1516 fixture_defs.insert(fixture_name.clone(), def.clone());
1517 let valid_deps: Vec<String> = def
1519 .dependencies
1520 .iter()
1521 .filter(|d| self.definitions.contains_key(*d))
1522 .cloned()
1523 .collect();
1524 dep_graph.insert(fixture_name, valid_deps);
1525 }
1526 }
1527
1528 let mut cycles = Vec::new();
1529 let mut visited: HashSet<String> = HashSet::new();
1530 let mut seen_cycles: HashSet<String> = HashSet::new(); for start_fixture in dep_graph.keys() {
1534 if visited.contains(start_fixture) {
1535 continue;
1536 }
1537
1538 let mut stack: Vec<(String, usize, Vec<String>)> =
1540 vec![(start_fixture.clone(), 0, vec![])];
1541 let mut rec_stack: HashSet<String> = HashSet::new();
1542
1543 while let Some((current, idx, mut path)) = stack.pop() {
1544 if idx == 0 {
1545 if rec_stack.contains(¤t) {
1547 let cycle_start_idx = path.iter().position(|f| f == ¤t).unwrap_or(0);
1549 let mut cycle_path: Vec<String> = path[cycle_start_idx..].to_vec();
1550 cycle_path.push(current.clone());
1551
1552 let mut cycle_key: Vec<String> =
1554 cycle_path[..cycle_path.len() - 1].to_vec();
1555 cycle_key.sort();
1556 let cycle_key_str = cycle_key.join(",");
1557
1558 if !seen_cycles.contains(&cycle_key_str) {
1559 seen_cycles.insert(cycle_key_str);
1560 if let Some(fixture_def) = fixture_defs.get(¤t) {
1561 cycles.push(FixtureCycle {
1562 cycle_path,
1563 fixture: fixture_def.clone(),
1564 });
1565 }
1566 }
1567 continue;
1568 }
1569
1570 rec_stack.insert(current.clone());
1571 path.push(current.clone());
1572 }
1573
1574 let deps = match dep_graph.get(¤t) {
1576 Some(d) => d,
1577 None => {
1578 rec_stack.remove(¤t);
1579 continue;
1580 }
1581 };
1582
1583 if idx < deps.len() {
1584 stack.push((current.clone(), idx + 1, path.clone()));
1586
1587 let dep = &deps[idx];
1588 if rec_stack.contains(dep) {
1589 let cycle_start_idx = path.iter().position(|f| f == dep).unwrap_or(0);
1591 let mut cycle_path: Vec<String> = path[cycle_start_idx..].to_vec();
1592 cycle_path.push(dep.clone());
1593
1594 let mut cycle_key: Vec<String> =
1595 cycle_path[..cycle_path.len() - 1].to_vec();
1596 cycle_key.sort();
1597 let cycle_key_str = cycle_key.join(",");
1598
1599 if !seen_cycles.contains(&cycle_key_str) {
1600 seen_cycles.insert(cycle_key_str);
1601 if let Some(fixture_def) = fixture_defs.get(dep) {
1602 cycles.push(FixtureCycle {
1603 cycle_path,
1604 fixture: fixture_def.clone(),
1605 });
1606 }
1607 }
1608 } else if !visited.contains(dep) {
1609 stack.push((dep.clone(), 0, path.clone()));
1611 }
1612 } else {
1613 visited.insert(current.clone());
1615 rec_stack.remove(¤t);
1616 }
1617 }
1618 }
1619
1620 cycles
1621 }
1622
1623 pub fn detect_fixture_cycles_in_file(
1627 &self,
1628 file_path: &Path,
1629 ) -> Vec<super::types::FixtureCycle> {
1630 let all_cycles = self.detect_fixture_cycles();
1631 all_cycles
1632 .iter()
1633 .filter(|cycle| cycle.fixture.file_path == file_path)
1634 .cloned()
1635 .collect()
1636 }
1637
1638 pub fn detect_scope_mismatches_in_file(
1644 &self,
1645 file_path: &Path,
1646 ) -> Vec<super::types::ScopeMismatch> {
1647 use super::types::ScopeMismatch;
1648
1649 let mut mismatches = Vec::new();
1650
1651 let Some(fixture_names) = self.file_definitions.get(file_path) else {
1653 return mismatches;
1654 };
1655
1656 for fixture_name in fixture_names.iter() {
1657 let Some(definitions) = self.definitions.get(fixture_name) else {
1659 continue;
1660 };
1661
1662 let Some(fixture_def) = definitions.iter().find(|d| d.file_path == file_path) else {
1664 continue;
1665 };
1666
1667 for dep_name in &fixture_def.dependencies {
1669 if let Some(dep_definitions) = self.definitions.get(dep_name) {
1671 if let Some(dep_def) = dep_definitions.first() {
1674 if fixture_def.scope > dep_def.scope {
1677 mismatches.push(ScopeMismatch {
1678 fixture: fixture_def.clone(),
1679 dependency: dep_def.clone(),
1680 });
1681 }
1682 }
1683 }
1684 }
1685 }
1686
1687 mismatches
1688 }
1689
1690 pub fn resolve_fixture_for_file(
1695 &self,
1696 file_path: &Path,
1697 fixture_name: &str,
1698 ) -> Option<FixtureDefinition> {
1699 let definitions = self.definitions.get(fixture_name)?;
1700
1701 if let Some(def) = definitions.iter().find(|d| d.file_path == file_path) {
1703 return Some(def.clone());
1704 }
1705
1706 let file_path = self.get_canonical_path(file_path.to_path_buf());
1708 let mut best_conftest: Option<&FixtureDefinition> = None;
1709 let mut best_depth = usize::MAX;
1710
1711 for def in definitions.iter() {
1712 if def.is_third_party {
1713 continue;
1714 }
1715 if def.file_path.ends_with("conftest.py") {
1716 if let Some(parent) = def.file_path.parent() {
1717 if file_path.starts_with(parent) {
1718 let depth = parent.components().count();
1719 if depth > best_depth {
1720 best_conftest = Some(def);
1722 best_depth = depth;
1723 } else if best_conftest.is_none() {
1724 best_conftest = Some(def);
1725 best_depth = depth;
1726 }
1727 }
1728 }
1729 }
1730 }
1731
1732 if let Some(def) = best_conftest {
1733 return Some(def.clone());
1734 }
1735
1736 if let Some(def) = definitions
1738 .iter()
1739 .find(|d| d.is_plugin && !d.is_third_party)
1740 {
1741 return Some(def.clone());
1742 }
1743
1744 if let Some(def) = definitions.iter().find(|d| d.is_third_party) {
1746 return Some(def.clone());
1747 }
1748
1749 definitions.first().cloned()
1751 }
1752
1753 pub fn find_containing_function(&self, file_path: &Path, line: usize) -> Option<String> {
1757 let content = self.get_file_content(file_path)?;
1758
1759 let parsed = self.get_parsed_ast(file_path, &content)?;
1761
1762 if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
1763 let line_index = self.get_line_index(file_path, &content);
1765
1766 for stmt in &module.body {
1767 if let Some(name) = self.find_function_containing_line(stmt, line, &line_index) {
1768 return Some(name);
1769 }
1770 }
1771 }
1772
1773 None
1774 }
1775
1776 fn find_function_containing_line(
1778 &self,
1779 stmt: &Stmt,
1780 target_line: usize,
1781 line_index: &[usize],
1782 ) -> Option<String> {
1783 match stmt {
1784 Stmt::FunctionDef(func_def) => {
1785 let start_line =
1786 self.get_line_from_offset(func_def.range.start().to_usize(), line_index);
1787 let end_line =
1788 self.get_line_from_offset(func_def.range.end().to_usize(), line_index);
1789
1790 if target_line >= start_line && target_line <= end_line {
1791 return Some(func_def.name.to_string());
1792 }
1793 }
1794 Stmt::AsyncFunctionDef(func_def) => {
1795 let start_line =
1796 self.get_line_from_offset(func_def.range.start().to_usize(), line_index);
1797 let end_line =
1798 self.get_line_from_offset(func_def.range.end().to_usize(), line_index);
1799
1800 if target_line >= start_line && target_line <= end_line {
1801 return Some(func_def.name.to_string());
1802 }
1803 }
1804 Stmt::ClassDef(class_def) => {
1805 for class_stmt in &class_def.body {
1807 if let Some(name) =
1808 self.find_function_containing_line(class_stmt, target_line, line_index)
1809 {
1810 return Some(name);
1811 }
1812 }
1813 }
1814 _ => {}
1815 }
1816 None
1817 }
1818}