1use super::decorators;
7use super::types::{
8 CompletionContext, FixtureDefinition, FixtureScope, FixtureUsage, ParamInsertionInfo,
9 UndeclaredFixture,
10};
11use super::FixtureDatabase;
12use rustpython_parser::ast::{Arguments, 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(
1328 &self,
1329 file_path: &Path,
1330 function_line: usize,
1331 ) -> Option<ParamInsertionInfo> {
1332 let content = self.get_file_content(file_path)?;
1333 let line_index = self.get_line_index(file_path, &content);
1334 let bytes = content.as_bytes();
1335
1336 if let Some(ast) = self.get_parsed_ast(file_path, &content) {
1340 if let rustpython_parser::ast::Mod::Module(module) = ast.as_ref() {
1341 if let Some(info) =
1342 find_insertion_in_stmts(&module.body, function_line, bytes, &line_index)
1343 {
1344 return Some(info);
1345 }
1346 }
1347 }
1348
1349 let def_line_start = *line_index
1353 .get(function_line.saturating_sub(1))
1354 .unwrap_or(&0);
1355 let close_paren = scan_for_signature_close_paren(bytes, def_line_start)?;
1356
1357 let open_paren = bytes[def_line_start..close_paren]
1359 .iter()
1360 .position(|&b| b == b'(')
1361 .map(|pos| def_line_start + pos)?;
1362
1363 let between = &bytes[open_paren + 1..close_paren];
1365 let has_params = {
1366 let mut in_comment = false;
1367 between.iter().any(|&b| {
1368 if b == b'\n' {
1369 in_comment = false;
1370 return false;
1371 }
1372 if in_comment {
1373 return false;
1374 }
1375 if b == b'#' {
1376 in_comment = true;
1377 return false;
1378 }
1379 !b.is_ascii_whitespace()
1380 })
1381 };
1382
1383 let close_paren_line = byte_offset_to_line_1based(close_paren, &line_index);
1388 if has_params {
1389 if let Some(ml) =
1390 try_multiline_insertion(close_paren, close_paren_line, bytes, &line_index)
1391 {
1392 return Some(ml);
1393 }
1394 }
1395
1396 Some(ParamInsertionInfo {
1397 line: close_paren_line,
1398 char_pos: byte_offset_to_col(close_paren, &line_index),
1399 needs_comma: has_params,
1400 multiline_indent: None,
1401 })
1402 }
1403
1404 #[allow(dead_code)] pub fn is_inside_function(
1408 &self,
1409 file_path: &Path,
1410 line: u32,
1411 character: u32,
1412 ) -> Option<(String, bool, Vec<String>)> {
1413 let content = self.get_file_content(file_path)?;
1415
1416 let target_line = (line + 1) as usize; let parsed = self.get_parsed_ast(file_path, &content)?;
1420
1421 if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
1422 return self.find_enclosing_function(
1423 &module.body,
1424 &content,
1425 target_line,
1426 character as usize,
1427 );
1428 }
1429
1430 None
1431 }
1432
1433 #[allow(dead_code)]
1434 fn find_enclosing_function(
1435 &self,
1436 stmts: &[Stmt],
1437 content: &str,
1438 target_line: usize,
1439 _target_char: usize,
1440 ) -> Option<(String, bool, Vec<String>)> {
1441 let line_index = Self::build_line_index(content);
1442
1443 for stmt in stmts {
1444 match stmt {
1445 Stmt::FunctionDef(func_def) => {
1446 let func_start_line =
1447 self.get_line_from_offset(func_def.range.start().to_usize(), &line_index);
1448 let func_end_line =
1449 self.get_line_from_offset(func_def.range.end().to_usize(), &line_index);
1450
1451 if target_line >= func_start_line && target_line <= func_end_line {
1453 let is_fixture = func_def
1454 .decorator_list
1455 .iter()
1456 .any(decorators::is_fixture_decorator);
1457 let is_test = func_def.name.starts_with("test_");
1458
1459 if is_test || is_fixture {
1461 let params: Vec<String> = func_def
1462 .args
1463 .args
1464 .iter()
1465 .map(|arg| arg.def.arg.to_string())
1466 .collect();
1467
1468 return Some((func_def.name.to_string(), is_fixture, params));
1469 }
1470 }
1471 }
1472 Stmt::AsyncFunctionDef(func_def) => {
1473 let func_start_line =
1474 self.get_line_from_offset(func_def.range.start().to_usize(), &line_index);
1475 let func_end_line =
1476 self.get_line_from_offset(func_def.range.end().to_usize(), &line_index);
1477
1478 if target_line >= func_start_line && target_line <= func_end_line {
1479 let is_fixture = func_def
1480 .decorator_list
1481 .iter()
1482 .any(decorators::is_fixture_decorator);
1483 let is_test = func_def.name.starts_with("test_");
1484
1485 if is_test || is_fixture {
1486 let params: Vec<String> = func_def
1487 .args
1488 .args
1489 .iter()
1490 .map(|arg| arg.def.arg.to_string())
1491 .collect();
1492
1493 return Some((func_def.name.to_string(), is_fixture, params));
1494 }
1495 }
1496 }
1497 _ => {}
1498 }
1499 }
1500
1501 None
1502 }
1503
1504 pub fn detect_fixture_cycles(&self) -> std::sync::Arc<Vec<super::types::FixtureCycle>> {
1510 use std::sync::Arc;
1511
1512 let current_version = self
1513 .definitions_version
1514 .load(std::sync::atomic::Ordering::SeqCst);
1515
1516 if let Some(cached) = self.cycle_cache.get(&()) {
1518 let (cached_version, cached_cycles) = cached.value();
1519 if *cached_version == current_version {
1520 return Arc::clone(cached_cycles);
1521 }
1522 }
1523
1524 let cycles = Arc::new(self.compute_fixture_cycles());
1526
1527 self.cycle_cache
1529 .insert((), (current_version, Arc::clone(&cycles)));
1530
1531 cycles
1532 }
1533
1534 fn compute_fixture_cycles(&self) -> Vec<super::types::FixtureCycle> {
1537 use super::types::FixtureCycle;
1538 use std::collections::HashMap;
1539
1540 let mut dep_graph: HashMap<String, Vec<String>> = HashMap::new();
1542 let mut fixture_defs: HashMap<String, FixtureDefinition> = HashMap::new();
1543
1544 for entry in self.definitions.iter() {
1545 let fixture_name = entry.key().clone();
1546 if let Some(def) = entry.value().first() {
1547 fixture_defs.insert(fixture_name.clone(), def.clone());
1548 let valid_deps: Vec<String> = def
1550 .dependencies
1551 .iter()
1552 .filter(|d| self.definitions.contains_key(*d))
1553 .cloned()
1554 .collect();
1555 dep_graph.insert(fixture_name, valid_deps);
1556 }
1557 }
1558
1559 let mut cycles = Vec::new();
1560 let mut visited: HashSet<String> = HashSet::new();
1561 let mut seen_cycles: HashSet<String> = HashSet::new(); for start_fixture in dep_graph.keys() {
1565 if visited.contains(start_fixture) {
1566 continue;
1567 }
1568
1569 let mut stack: Vec<(String, usize, Vec<String>)> =
1571 vec![(start_fixture.clone(), 0, vec![])];
1572 let mut rec_stack: HashSet<String> = HashSet::new();
1573
1574 while let Some((current, idx, mut path)) = stack.pop() {
1575 if idx == 0 {
1576 if rec_stack.contains(¤t) {
1578 let cycle_start_idx = path.iter().position(|f| f == ¤t).unwrap_or(0);
1580 let mut cycle_path: Vec<String> = path[cycle_start_idx..].to_vec();
1581 cycle_path.push(current.clone());
1582
1583 let mut cycle_key: Vec<String> =
1585 cycle_path[..cycle_path.len() - 1].to_vec();
1586 cycle_key.sort();
1587 let cycle_key_str = cycle_key.join(",");
1588
1589 if !seen_cycles.contains(&cycle_key_str) {
1590 seen_cycles.insert(cycle_key_str);
1591 if let Some(fixture_def) = fixture_defs.get(¤t) {
1592 cycles.push(FixtureCycle {
1593 cycle_path,
1594 fixture: fixture_def.clone(),
1595 });
1596 }
1597 }
1598 continue;
1599 }
1600
1601 rec_stack.insert(current.clone());
1602 path.push(current.clone());
1603 }
1604
1605 let deps = match dep_graph.get(¤t) {
1607 Some(d) => d,
1608 None => {
1609 rec_stack.remove(¤t);
1610 continue;
1611 }
1612 };
1613
1614 if idx < deps.len() {
1615 stack.push((current.clone(), idx + 1, path.clone()));
1617
1618 let dep = &deps[idx];
1619 if rec_stack.contains(dep) {
1620 let cycle_start_idx = path.iter().position(|f| f == dep).unwrap_or(0);
1622 let mut cycle_path: Vec<String> = path[cycle_start_idx..].to_vec();
1623 cycle_path.push(dep.clone());
1624
1625 let mut cycle_key: Vec<String> =
1626 cycle_path[..cycle_path.len() - 1].to_vec();
1627 cycle_key.sort();
1628 let cycle_key_str = cycle_key.join(",");
1629
1630 if !seen_cycles.contains(&cycle_key_str) {
1631 seen_cycles.insert(cycle_key_str);
1632 if let Some(fixture_def) = fixture_defs.get(dep) {
1633 cycles.push(FixtureCycle {
1634 cycle_path,
1635 fixture: fixture_def.clone(),
1636 });
1637 }
1638 }
1639 } else if !visited.contains(dep) {
1640 stack.push((dep.clone(), 0, path.clone()));
1642 }
1643 } else {
1644 visited.insert(current.clone());
1646 rec_stack.remove(¤t);
1647 }
1648 }
1649 }
1650
1651 cycles
1652 }
1653
1654 pub fn detect_fixture_cycles_in_file(
1658 &self,
1659 file_path: &Path,
1660 ) -> Vec<super::types::FixtureCycle> {
1661 let all_cycles = self.detect_fixture_cycles();
1662 all_cycles
1663 .iter()
1664 .filter(|cycle| cycle.fixture.file_path == file_path)
1665 .cloned()
1666 .collect()
1667 }
1668
1669 pub fn detect_scope_mismatches_in_file(
1675 &self,
1676 file_path: &Path,
1677 ) -> Vec<super::types::ScopeMismatch> {
1678 use super::types::ScopeMismatch;
1679
1680 let mut mismatches = Vec::new();
1681
1682 let Some(fixture_names) = self.file_definitions.get(file_path) else {
1684 return mismatches;
1685 };
1686
1687 for fixture_name in fixture_names.iter() {
1688 let Some(definitions) = self.definitions.get(fixture_name) else {
1690 continue;
1691 };
1692
1693 let Some(fixture_def) = definitions.iter().find(|d| d.file_path == file_path) else {
1695 continue;
1696 };
1697
1698 for dep_name in &fixture_def.dependencies {
1700 if let Some(dep_definitions) = self.definitions.get(dep_name) {
1702 if let Some(dep_def) = dep_definitions.first() {
1705 if fixture_def.scope > dep_def.scope {
1708 mismatches.push(ScopeMismatch {
1709 fixture: fixture_def.clone(),
1710 dependency: dep_def.clone(),
1711 });
1712 }
1713 }
1714 }
1715 }
1716 }
1717
1718 mismatches
1719 }
1720
1721 pub fn resolve_fixture_for_file(
1726 &self,
1727 file_path: &Path,
1728 fixture_name: &str,
1729 ) -> Option<FixtureDefinition> {
1730 let definitions = self.definitions.get(fixture_name)?;
1731
1732 if let Some(def) = definitions.iter().find(|d| d.file_path == file_path) {
1734 return Some(def.clone());
1735 }
1736
1737 let file_path = self.get_canonical_path(file_path.to_path_buf());
1739 let mut best_conftest: Option<&FixtureDefinition> = None;
1740 let mut best_depth = usize::MAX;
1741
1742 for def in definitions.iter() {
1743 if def.is_third_party {
1744 continue;
1745 }
1746 if def.file_path.ends_with("conftest.py") {
1747 if let Some(parent) = def.file_path.parent() {
1748 if file_path.starts_with(parent) {
1749 let depth = parent.components().count();
1750 if depth > best_depth {
1751 best_conftest = Some(def);
1753 best_depth = depth;
1754 } else if best_conftest.is_none() {
1755 best_conftest = Some(def);
1756 best_depth = depth;
1757 }
1758 }
1759 }
1760 }
1761 }
1762
1763 if let Some(def) = best_conftest {
1764 return Some(def.clone());
1765 }
1766
1767 if let Some(def) = definitions
1769 .iter()
1770 .find(|d| d.is_plugin && !d.is_third_party)
1771 {
1772 return Some(def.clone());
1773 }
1774
1775 if let Some(def) = definitions.iter().find(|d| d.is_third_party) {
1777 return Some(def.clone());
1778 }
1779
1780 definitions.first().cloned()
1782 }
1783
1784 pub fn find_containing_function(&self, file_path: &Path, line: usize) -> Option<String> {
1788 let content = self.get_file_content(file_path)?;
1789
1790 let parsed = self.get_parsed_ast(file_path, &content)?;
1792
1793 if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
1794 let line_index = self.get_line_index(file_path, &content);
1796
1797 for stmt in &module.body {
1798 if let Some(name) = self.find_function_containing_line(stmt, line, &line_index) {
1799 return Some(name);
1800 }
1801 }
1802 }
1803
1804 None
1805 }
1806
1807 fn find_function_containing_line(
1809 &self,
1810 stmt: &Stmt,
1811 target_line: usize,
1812 line_index: &[usize],
1813 ) -> Option<String> {
1814 match stmt {
1815 Stmt::FunctionDef(func_def) => {
1816 let start_line =
1817 self.get_line_from_offset(func_def.range.start().to_usize(), line_index);
1818 let end_line =
1819 self.get_line_from_offset(func_def.range.end().to_usize(), line_index);
1820
1821 if target_line >= start_line && target_line <= end_line {
1822 return Some(func_def.name.to_string());
1823 }
1824 }
1825 Stmt::AsyncFunctionDef(func_def) => {
1826 let start_line =
1827 self.get_line_from_offset(func_def.range.start().to_usize(), line_index);
1828 let end_line =
1829 self.get_line_from_offset(func_def.range.end().to_usize(), line_index);
1830
1831 if target_line >= start_line && target_line <= end_line {
1832 return Some(func_def.name.to_string());
1833 }
1834 }
1835 Stmt::ClassDef(class_def) => {
1836 for class_stmt in &class_def.body {
1838 if let Some(name) =
1839 self.find_function_containing_line(class_stmt, target_line, line_index)
1840 {
1841 return Some(name);
1842 }
1843 }
1844 }
1845 _ => {}
1846 }
1847 None
1848 }
1849}
1850
1851fn scan_for_signature_close_paren(bytes: &[u8], start: usize) -> Option<usize> {
1863 let mut i = start;
1864 let mut depth: i32 = 0;
1865 let mut found_open = false;
1866
1867 while i < bytes.len() {
1868 match bytes[i] {
1869 b'#' => {
1870 while i < bytes.len() && bytes[i] != b'\n' {
1872 i += 1;
1873 }
1874 }
1875 b'"' | b'\'' => {
1876 let q = bytes[i];
1877 if i + 2 < bytes.len() && bytes[i + 1] == q && bytes[i + 2] == q {
1879 i += 3;
1880 while i < bytes.len() {
1881 if i + 2 < bytes.len()
1882 && bytes[i] == q
1883 && bytes[i + 1] == q
1884 && bytes[i + 2] == q
1885 {
1886 i += 3;
1887 break;
1888 }
1889 i += 1;
1890 }
1891 } else {
1892 i += 1;
1894 while i < bytes.len() {
1895 if bytes[i] == b'\\' {
1896 i += 2; } else if bytes[i] == q {
1898 i += 1; break;
1900 } else {
1901 i += 1;
1902 }
1903 }
1904 }
1905 }
1906 b'(' => {
1907 depth += 1;
1908 found_open = true;
1909 i += 1;
1910 }
1911 b')' if found_open => {
1912 depth -= 1;
1913 if depth == 0 {
1914 return Some(i);
1915 }
1916 i += 1;
1917 }
1918 _ => {
1919 i += 1;
1920 }
1921 }
1922 }
1923
1924 None
1925}
1926
1927fn byte_offset_to_line_1based(offset: usize, line_index: &[usize]) -> usize {
1933 match line_index.binary_search(&offset) {
1934 Ok(line) => line + 1,
1935 Err(line) => line,
1936 }
1937}
1938
1939fn byte_offset_to_col(offset: usize, line_index: &[usize]) -> usize {
1941 let line = byte_offset_to_line_1based(offset, line_index);
1942 offset - line_index[line.saturating_sub(1)]
1944}
1945
1946fn find_insertion_in_stmts(
1950 stmts: &[Stmt],
1951 function_line: usize,
1952 bytes: &[u8],
1953 line_index: &[usize],
1954) -> Option<ParamInsertionInfo> {
1955 for stmt in stmts {
1956 if let Some(info) = find_insertion_in_stmt(stmt, function_line, bytes, line_index) {
1957 return Some(info);
1958 }
1959 }
1960 None
1961}
1962
1963fn find_insertion_in_stmt(
1965 stmt: &Stmt,
1966 function_line: usize,
1967 bytes: &[u8],
1968 line_index: &[usize],
1969) -> Option<ParamInsertionInfo> {
1970 match stmt {
1971 Stmt::FunctionDef(f) => {
1972 let def_start = f.range.start().to_usize();
1973 if byte_offset_to_line_1based(def_start, line_index) == function_line {
1974 return param_insertion_from_args(def_start, &f.args, bytes, line_index);
1975 }
1976 find_insertion_in_stmts(&f.body, function_line, bytes, line_index)
1978 }
1979 Stmt::AsyncFunctionDef(f) => {
1980 let def_start = f.range.start().to_usize();
1981 if byte_offset_to_line_1based(def_start, line_index) == function_line {
1982 return param_insertion_from_args(def_start, &f.args, bytes, line_index);
1983 }
1984 find_insertion_in_stmts(&f.body, function_line, bytes, line_index)
1985 }
1986 Stmt::ClassDef(c) => {
1987 find_insertion_in_stmts(&c.body, function_line, bytes, line_index)
1989 }
1990 _ => None,
1991 }
1992}
1993
1994fn param_insertion_from_args(
2001 def_start: usize,
2002 args: &Arguments,
2003 bytes: &[u8],
2004 line_index: &[usize],
2005) -> Option<ParamInsertionInfo> {
2006 let has_params = !args.posonlyargs.is_empty()
2007 || !args.args.is_empty()
2008 || !args.kwonlyargs.is_empty()
2009 || args.vararg.is_some()
2010 || args.kwarg.is_some();
2011
2012 let close_paren = scan_for_signature_close_paren(bytes, def_start)?;
2013 let close_paren_line = byte_offset_to_line_1based(close_paren, line_index);
2014
2015 if has_params {
2019 if let Some(ml) = try_multiline_insertion(close_paren, close_paren_line, bytes, line_index)
2020 {
2021 return Some(ml);
2022 }
2023 }
2024
2025 Some(ParamInsertionInfo {
2026 line: close_paren_line,
2027 char_pos: byte_offset_to_col(close_paren, line_index),
2028 needs_comma: has_params,
2029 multiline_indent: None,
2030 })
2031}
2032
2033fn try_multiline_insertion(
2050 close_paren: usize,
2051 close_paren_line: usize,
2052 bytes: &[u8],
2053 line_index: &[usize],
2054) -> Option<ParamInsertionInfo> {
2055 let line_start = line_index[close_paren_line - 1];
2057 let only_ws = bytes[line_start..close_paren]
2058 .iter()
2059 .all(|&b| b == b' ' || b == b'\t');
2060 if !only_ws {
2061 return None;
2062 }
2063
2064 let last_content_pos = find_last_content_before(bytes, close_paren)?;
2068 let has_trailing_comma = bytes[last_content_pos] == b',';
2069
2070 let indent = indent_of_line_at(bytes, last_content_pos, line_index);
2073
2074 let insert_offset = last_content_pos + 1;
2077 let insert_line = byte_offset_to_line_1based(insert_offset, line_index);
2078 let insert_col = byte_offset_to_col(insert_offset, line_index);
2079
2080 Some(ParamInsertionInfo {
2081 line: insert_line,
2082 char_pos: insert_col,
2083 needs_comma: !has_trailing_comma,
2088 multiline_indent: Some(indent),
2089 })
2090}
2091
2092fn find_last_content_before(bytes: &[u8], before: usize) -> Option<usize> {
2097 let mut pos = before;
2098 while pos > 0 {
2099 pos -= 1;
2100 match bytes[pos] {
2101 b' ' | b'\t' | b'\n' | b'\r' => continue,
2102 _ => return Some(pos),
2103 }
2104 }
2105 None
2106}
2107
2108fn indent_of_line_at(bytes: &[u8], byte_pos: usize, line_index: &[usize]) -> String {
2110 let line_1based = byte_offset_to_line_1based(byte_pos, line_index);
2111 let line_start = line_index[line_1based - 1];
2112 let indent_len = bytes[line_start..]
2113 .iter()
2114 .take_while(|&&b| b == b' ' || b == b'\t')
2115 .count();
2116 String::from_utf8_lossy(&bytes[line_start..line_start + indent_len]).into_owned()
2117}