1use super::decorators;
8use super::types::{FixtureDefinition, FixtureUsage};
9use super::FixtureDatabase;
10use rustpython_parser::ast::{ArgWithDefault, Arguments, Expr, Stmt};
11use rustpython_parser::{parse, Mode};
12use std::collections::HashSet;
13use std::path::{Path, PathBuf};
14use tracing::{debug, error, info};
15
16impl FixtureDatabase {
17 pub fn analyze_file(&self, file_path: PathBuf, content: &str) {
20 self.analyze_file_internal(file_path, content, true);
21 }
22
23 pub(crate) fn analyze_file_fresh(&self, file_path: PathBuf, content: &str) {
26 self.analyze_file_internal(file_path, content, false);
27 }
28
29 fn analyze_file_internal(&self, file_path: PathBuf, content: &str, cleanup_previous: bool) {
31 let file_path = self.get_canonical_path(file_path);
33
34 debug!("Analyzing file: {:?}", file_path);
35
36 self.file_cache
39 .insert(file_path.clone(), std::sync::Arc::new(content.to_string()));
40
41 let parsed = match parse(content, Mode::Module, "") {
43 Ok(ast) => ast,
44 Err(e) => {
45 error!("Failed to parse Python file {:?}: {}", file_path, e);
46 return;
47 }
48 };
49
50 self.usages.remove(&file_path);
52
53 self.undeclared_fixtures.remove(&file_path);
55
56 self.imports.remove(&file_path);
58
59 if cleanup_previous {
66 self.cleanup_definitions_for_file(&file_path);
67 }
68
69 let is_conftest = file_path
71 .file_name()
72 .map(|n| n == "conftest.py")
73 .unwrap_or(false);
74 debug!("is_conftest: {}", is_conftest);
75
76 let line_index = self.get_line_index(&file_path, content);
78
79 if let rustpython_parser::ast::Mod::Module(module) = parsed {
81 debug!("Module has {} statements", module.body.len());
82
83 let mut module_level_names = HashSet::new();
85 for stmt in &module.body {
86 self.collect_module_level_names(stmt, &mut module_level_names);
87 }
88 self.imports.insert(file_path.clone(), module_level_names);
89
90 for stmt in &module.body {
92 self.visit_stmt(stmt, &file_path, is_conftest, content, &line_index);
93 }
94 }
95
96 debug!("Analysis complete for {:?}", file_path);
97 }
98
99 fn cleanup_definitions_for_file(&self, file_path: &PathBuf) {
109 let fixture_names = match self.file_definitions.remove(file_path) {
111 Some((_, names)) => names,
112 None => return, };
114
115 for fixture_name in fixture_names {
117 let should_remove = {
118 if let Some(mut defs) = self.definitions.get_mut(&fixture_name) {
120 defs.retain(|def| def.file_path != *file_path);
121 defs.is_empty()
122 } else {
123 false
124 }
125 }; if should_remove {
129 self.definitions
132 .remove_if(&fixture_name, |_, defs| defs.is_empty());
133 }
134 }
135 }
136
137 pub(crate) fn build_line_index(content: &str) -> Vec<usize> {
139 let mut line_index = Vec::with_capacity(content.len() / 30);
140 line_index.push(0);
141 for (i, c) in content.char_indices() {
142 if c == '\n' {
143 line_index.push(i + 1);
144 }
145 }
146 line_index
147 }
148
149 pub(crate) fn get_line_from_offset(&self, offset: usize, line_index: &[usize]) -> usize {
151 match line_index.binary_search(&offset) {
152 Ok(line) => line + 1,
153 Err(line) => line,
154 }
155 }
156
157 pub(crate) fn get_char_position_from_offset(
159 &self,
160 offset: usize,
161 line_index: &[usize],
162 ) -> usize {
163 let line = self.get_line_from_offset(offset, line_index);
164 let line_start = line_index[line - 1];
165 offset.saturating_sub(line_start)
166 }
167
168 pub(crate) fn all_args(args: &Arguments) -> impl Iterator<Item = &ArgWithDefault> {
172 args.posonlyargs
173 .iter()
174 .chain(args.args.iter())
175 .chain(args.kwonlyargs.iter())
176 }
177
178 fn record_fixture_usage(
181 &self,
182 file_path: &Path,
183 fixture_name: String,
184 line: usize,
185 start_char: usize,
186 end_char: usize,
187 ) {
188 let file_path_buf = file_path.to_path_buf();
189 let usage = FixtureUsage {
190 name: fixture_name,
191 file_path: file_path_buf.clone(),
192 line,
193 start_char,
194 end_char,
195 };
196 self.usages.entry(file_path_buf).or_default().push(usage);
197 }
198
199 fn record_fixture_definition(&self, definition: FixtureDefinition) {
202 let file_path = definition.file_path.clone();
203 let fixture_name = definition.name.clone();
204
205 self.definitions
207 .entry(fixture_name.clone())
208 .or_default()
209 .push(definition);
210
211 self.file_definitions
213 .entry(file_path)
214 .or_default()
215 .insert(fixture_name);
216 }
217
218 fn visit_stmt(
220 &self,
221 stmt: &Stmt,
222 file_path: &PathBuf,
223 _is_conftest: bool,
224 content: &str,
225 line_index: &[usize],
226 ) {
227 if let Stmt::Assign(assign) = stmt {
229 self.visit_assignment_fixture(assign, file_path, content, line_index);
230 }
231
232 if let Stmt::ClassDef(class_def) = stmt {
234 for decorator in &class_def.decorator_list {
236 let usefixtures = decorators::extract_usefixtures_names(decorator);
237 for (fixture_name, range) in usefixtures {
238 let usage_line =
239 self.get_line_from_offset(range.start().to_usize(), line_index);
240 let start_char =
241 self.get_char_position_from_offset(range.start().to_usize(), line_index);
242 let end_char =
243 self.get_char_position_from_offset(range.end().to_usize(), line_index);
244
245 info!(
246 "Found usefixtures usage on class: {} at {:?}:{}:{}",
247 fixture_name, file_path, usage_line, start_char
248 );
249
250 self.record_fixture_usage(
251 file_path,
252 fixture_name,
253 usage_line,
254 start_char + 1,
255 end_char - 1,
256 );
257 }
258 }
259
260 for class_stmt in &class_def.body {
261 self.visit_stmt(class_stmt, file_path, _is_conftest, content, line_index);
262 }
263 return;
264 }
265
266 let (func_name, decorator_list, args, range, body, returns) = match stmt {
268 Stmt::FunctionDef(func_def) => (
269 func_def.name.as_str(),
270 &func_def.decorator_list,
271 &func_def.args,
272 func_def.range,
273 &func_def.body,
274 &func_def.returns,
275 ),
276 Stmt::AsyncFunctionDef(func_def) => (
277 func_def.name.as_str(),
278 &func_def.decorator_list,
279 &func_def.args,
280 func_def.range,
281 &func_def.body,
282 &func_def.returns,
283 ),
284 _ => return,
285 };
286
287 debug!("Found function: {}", func_name);
288
289 for decorator in decorator_list {
291 let usefixtures = decorators::extract_usefixtures_names(decorator);
292 for (fixture_name, range) in usefixtures {
293 let usage_line = self.get_line_from_offset(range.start().to_usize(), line_index);
294 let start_char =
295 self.get_char_position_from_offset(range.start().to_usize(), line_index);
296 let end_char =
297 self.get_char_position_from_offset(range.end().to_usize(), line_index);
298
299 info!(
300 "Found usefixtures usage on function: {} at {:?}:{}:{}",
301 fixture_name, file_path, usage_line, start_char
302 );
303
304 self.record_fixture_usage(
305 file_path,
306 fixture_name,
307 usage_line,
308 start_char + 1,
309 end_char - 1,
310 );
311 }
312 }
313
314 for decorator in decorator_list {
316 let indirect_fixtures = decorators::extract_parametrize_indirect_fixtures(decorator);
317 for (fixture_name, range) in indirect_fixtures {
318 let usage_line = self.get_line_from_offset(range.start().to_usize(), line_index);
319 let start_char =
320 self.get_char_position_from_offset(range.start().to_usize(), line_index);
321 let end_char =
322 self.get_char_position_from_offset(range.end().to_usize(), line_index);
323
324 info!(
325 "Found parametrize indirect fixture usage: {} at {:?}:{}:{}",
326 fixture_name, file_path, usage_line, start_char
327 );
328
329 self.record_fixture_usage(
330 file_path,
331 fixture_name,
332 usage_line,
333 start_char + 1,
334 end_char - 1,
335 );
336 }
337 }
338
339 debug!(
341 "Function {} has {} decorators",
342 func_name,
343 decorator_list.len()
344 );
345 let fixture_decorator = decorator_list
346 .iter()
347 .find(|dec| decorators::is_fixture_decorator(dec));
348
349 if let Some(decorator) = fixture_decorator {
350 debug!(" Decorator matched as fixture!");
351
352 let fixture_name = decorators::extract_fixture_name_from_decorator(decorator)
354 .unwrap_or_else(|| func_name.to_string());
355
356 let line = self.get_line_from_offset(range.start().to_usize(), line_index);
357 let docstring = self.extract_docstring(body);
358 let return_type = self.extract_return_type(returns, body, content);
359
360 info!(
361 "Found fixture definition: {} (function: {}) at {:?}:{}",
362 fixture_name, func_name, file_path, line
363 );
364
365 let (start_char, end_char) = self.find_function_name_position(content, line, func_name);
366
367 let is_third_party = file_path.to_string_lossy().contains("site-packages");
368 let definition = FixtureDefinition {
369 name: fixture_name.clone(),
370 file_path: file_path.clone(),
371 line,
372 start_char,
373 end_char,
374 docstring,
375 return_type,
376 is_third_party,
377 };
378
379 self.record_fixture_definition(definition);
380
381 let mut declared_params: HashSet<String> = HashSet::new();
383 declared_params.insert("self".to_string());
384 declared_params.insert("request".to_string());
385 declared_params.insert(func_name.to_string());
386
387 for arg in Self::all_args(args) {
388 let arg_name = arg.def.arg.as_str();
389 declared_params.insert(arg_name.to_string());
390
391 if arg_name != "self" && arg_name != "request" {
392 let arg_line =
393 self.get_line_from_offset(arg.def.range.start().to_usize(), line_index);
394 let start_char = self.get_char_position_from_offset(
395 arg.def.range.start().to_usize(),
396 line_index,
397 );
398 let end_char = self
399 .get_char_position_from_offset(arg.def.range.end().to_usize(), line_index);
400
401 info!(
402 "Found fixture dependency: {} at {:?}:{}:{}",
403 arg_name, file_path, arg_line, start_char
404 );
405
406 self.record_fixture_usage(
407 file_path,
408 arg_name.to_string(),
409 arg_line,
410 start_char,
411 end_char,
412 );
413 }
414 }
415
416 let function_line = self.get_line_from_offset(range.start().to_usize(), line_index);
417 self.scan_function_body_for_undeclared_fixtures(
418 body,
419 file_path,
420 line_index,
421 &declared_params,
422 func_name,
423 function_line,
424 );
425 }
426
427 let is_test = func_name.starts_with("test_");
429
430 if is_test {
431 debug!("Found test function: {}", func_name);
432
433 let mut declared_params: HashSet<String> = HashSet::new();
434 declared_params.insert("self".to_string());
435 declared_params.insert("request".to_string());
436
437 for arg in Self::all_args(args) {
438 let arg_name = arg.def.arg.as_str();
439 declared_params.insert(arg_name.to_string());
440
441 if arg_name != "self" {
442 let arg_offset = arg.def.range.start().to_usize();
443 let arg_line = self.get_line_from_offset(arg_offset, line_index);
444 let start_char = self.get_char_position_from_offset(arg_offset, line_index);
445 let end_char = self
446 .get_char_position_from_offset(arg.def.range.end().to_usize(), line_index);
447
448 debug!(
449 "Parameter {} at offset {}, calculated line {}, char {}",
450 arg_name, arg_offset, arg_line, start_char
451 );
452 info!(
453 "Found fixture usage: {} at {:?}:{}:{}",
454 arg_name, file_path, arg_line, start_char
455 );
456
457 self.record_fixture_usage(
458 file_path,
459 arg_name.to_string(),
460 arg_line,
461 start_char,
462 end_char,
463 );
464 }
465 }
466
467 let function_line = self.get_line_from_offset(range.start().to_usize(), line_index);
468 self.scan_function_body_for_undeclared_fixtures(
469 body,
470 file_path,
471 line_index,
472 &declared_params,
473 func_name,
474 function_line,
475 );
476 }
477 }
478
479 fn visit_assignment_fixture(
481 &self,
482 assign: &rustpython_parser::ast::StmtAssign,
483 file_path: &PathBuf,
484 _content: &str,
485 line_index: &[usize],
486 ) {
487 if let Expr::Call(outer_call) = &*assign.value {
488 if let Expr::Call(inner_call) = &*outer_call.func {
489 if decorators::is_fixture_decorator(&inner_call.func) {
490 for target in &assign.targets {
491 if let Expr::Name(name) = target {
492 let fixture_name = name.id.as_str();
493 let line = self
494 .get_line_from_offset(assign.range.start().to_usize(), line_index);
495
496 let start_char = self.get_char_position_from_offset(
497 name.range.start().to_usize(),
498 line_index,
499 );
500 let end_char = self.get_char_position_from_offset(
501 name.range.end().to_usize(),
502 line_index,
503 );
504
505 info!(
506 "Found fixture assignment: {} at {:?}:{}:{}-{}",
507 fixture_name, file_path, line, start_char, end_char
508 );
509
510 let is_third_party =
511 file_path.to_string_lossy().contains("site-packages");
512 let definition = FixtureDefinition {
513 name: fixture_name.to_string(),
514 file_path: file_path.clone(),
515 line,
516 start_char,
517 end_char,
518 docstring: None,
519 return_type: None,
520 is_third_party,
521 };
522
523 self.record_fixture_definition(definition);
524 }
525 }
526 }
527 }
528 }
529 }
530}
531
532impl FixtureDatabase {
534 fn collect_module_level_names(&self, stmt: &Stmt, names: &mut HashSet<String>) {
538 match stmt {
539 Stmt::Import(import_stmt) => {
540 for alias in &import_stmt.names {
541 let name = alias.asname.as_ref().unwrap_or(&alias.name);
542 names.insert(name.to_string());
543 }
544 }
545 Stmt::ImportFrom(import_from) => {
546 for alias in &import_from.names {
547 let name = alias.asname.as_ref().unwrap_or(&alias.name);
548 names.insert(name.to_string());
549 }
550 }
551 Stmt::FunctionDef(func_def) => {
552 let is_fixture = func_def
553 .decorator_list
554 .iter()
555 .any(decorators::is_fixture_decorator);
556 if !is_fixture {
557 names.insert(func_def.name.to_string());
558 }
559 }
560 Stmt::AsyncFunctionDef(func_def) => {
561 let is_fixture = func_def
562 .decorator_list
563 .iter()
564 .any(decorators::is_fixture_decorator);
565 if !is_fixture {
566 names.insert(func_def.name.to_string());
567 }
568 }
569 Stmt::ClassDef(class_def) => {
570 names.insert(class_def.name.to_string());
571 }
572 Stmt::Assign(assign) => {
573 for target in &assign.targets {
574 self.collect_names_from_expr(target, names);
575 }
576 }
577 Stmt::AnnAssign(ann_assign) => {
578 self.collect_names_from_expr(&ann_assign.target, names);
579 }
580 _ => {}
581 }
582 }
583
584 #[allow(clippy::only_used_in_recursion)]
585 pub(crate) fn collect_names_from_expr(&self, expr: &Expr, names: &mut HashSet<String>) {
586 match expr {
587 Expr::Name(name) => {
588 names.insert(name.id.to_string());
589 }
590 Expr::Tuple(tuple) => {
591 for elt in &tuple.elts {
592 self.collect_names_from_expr(elt, names);
593 }
594 }
595 Expr::List(list) => {
596 for elt in &list.elts {
597 self.collect_names_from_expr(elt, names);
598 }
599 }
600 _ => {}
601 }
602 }
603
604 fn find_function_name_position(
608 &self,
609 content: &str,
610 line: usize,
611 func_name: &str,
612 ) -> (usize, usize) {
613 super::string_utils::find_function_name_position(content, line, func_name)
614 }
615}
616
617