Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/fixtures/analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ impl FixtureDatabase {

/// Helper to record a fixture definition in the database.
/// Also maintains the file_definitions reverse index for efficient cleanup.
fn record_fixture_definition(&self, definition: FixtureDefinition) {
pub(crate) fn record_fixture_definition(&self, definition: FixtureDefinition) {
let file_path = definition.file_path.clone();
let fixture_name = definition.name.clone();

Expand Down Expand Up @@ -555,11 +555,15 @@ impl FixtureDatabase {

self.record_fixture_definition(definition);

// Record each dependency as a usage
// Record each parameter as a usage (dependencies + special builtins like
// `request` that are not fixture dependencies but need inlay hints / code actions)
for arg in Self::all_args(args) {
let arg_name = arg.def.arg.as_str();

if arg_name != "self" && arg_name != "request" {
// `request` is excluded from *dependencies* (it is a special pytest
// injection, not a regular fixture), but we DO record it as a usage
// so that inlay hints and type-annotation code actions work on it.
if arg_name != "self" {
Comment on lines 560 to +566
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After widening the condition to include request, the surrounding wording/logging still frames these as “dependencies” (e.g. the loop comment and the later info!("Found fixture dependency: ...")). Since request is explicitly excluded from the dependencies list above, consider renaming the comment/log to “parameter usage” (or only logging as dependency when arg_name != "request") to keep behavior and diagnostics consistent.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same, clarified the comment

let arg_line =
self.get_line_from_offset(arg.def.range.start().to_usize(), line_index);
let start_char = self.get_char_position_from_offset(
Expand All @@ -570,7 +574,7 @@ impl FixtureDatabase {
let end_char = start_char + arg_name.len();

info!(
"Found fixture dependency: {} at {:?}:{}:{}",
"Found fixture parameter usage: {} at {:?}:{}:{}",
arg_name, file_path, arg_line, start_char
);

Expand Down
77 changes: 77 additions & 0 deletions src/fixtures/scanner.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Workspace and virtual environment scanning for fixture definitions.

use super::imports::try_init_stdlib_from_python;
use super::types::{FixtureDefinition, FixtureScope, TypeImportSpec};
use super::FixtureDatabase;
use glob::Pattern;
use rayon::prelude::*;
Expand Down Expand Up @@ -749,6 +750,82 @@ impl FixtureDatabase {
pytest_internal
);
self.scan_plugin_directory(&pytest_internal);

// `request` is not defined via @pytest.fixture anywhere in _pytest/ —
// pytest injects it programmatically via FixtureManager. Register a
// synthetic definition so that hover, inlay hints, completion and code
// actions all know its type.
self.register_request_builtin_fixture(&pytest_internal);
}

/// Inject a hard-coded `FixtureDefinition` for the `request` fixture.
///
/// pytest's built-in `request` fixture is registered programmatically by
/// `FixtureManager` and therefore never appears as a `@pytest.fixture`-
/// decorated function that the AST scanner could pick up. We synthesise
/// the definition here so every LSP feature (hover, inlay hints,
/// completions, go-to-definition, code actions) can work with it.
///
/// The `file_path` is set to `_pytest/fixtures.py` when that file exists,
/// which gives a useful go-to-definition target. A sentinel path is used
/// as a fallback so the entry never gets accidentally cleared by a
/// subsequent `analyze_file` call on a real source file.
fn register_request_builtin_fixture(&self, pytest_internal: &Path) {
// Prefer the real _pytest/fixtures.py for go-to-definition.
let fixtures_py = pytest_internal.join("fixtures.py");
let file_path = if fixtures_py.exists() {
fixtures_py
.canonicalize()
.unwrap_or_else(|_| fixtures_py.clone())
} else {
// Sentinel path – will never be passed to analyze_file.
pytest_internal.join("_pytest_request_builtin.py")
};

// Guard: skip if we'd register an identical entry again (same path).
// Otherwise drop any stale synthetic entry first so a second scan that
// finds the real fixtures.py after a sentinel-path registration doesn't
// accumulate two entries.
if let Some(existing) = self.definitions.get("request") {
if existing.iter().any(|d| d.file_path == file_path) {
debug!(
"Synthetic 'request' fixture already registered for {:?}, skipping",
file_path
);
return;
}
}
drop(self.definitions.remove("request"));

let docstring = concat!(
"Special fixture providing information about the requesting test context.\n",
"\n",
"See https://docs.pytest.org/en/stable/reference/reference.html#request"
);

let definition = FixtureDefinition {
name: "request".to_string(),
file_path,
line: 1,
end_line: 1,
start_char: 0,
end_char: "request".len(),
docstring: Some(docstring.to_string()),
return_type: Some("FixtureRequest".to_string()),
return_type_imports: vec![TypeImportSpec {
check_name: "FixtureRequest".to_string(),
import_statement: "from pytest import FixtureRequest".to_string(),
}],
is_third_party: true,
is_plugin: true,
dependencies: vec![],
scope: FixtureScope::Function,
yield_line: None,
autouse: false,
};

info!("Registering synthetic 'request' fixture definition");
self.record_fixture_definition(definition);
}

/// Extract the raw and normalized package name from a `.dist-info` directory name.
Expand Down
Loading
Loading