Skip to main content

pytest_language_server/fixtures/
types.rs

1//! Data structures for fixture definitions, usages, and related types.
2
3use std::path::PathBuf;
4
5/// Specifies how to import a type referenced in a fixture's return annotation.
6///
7/// Resolved at analysis time from the fixture file's own imports, this struct
8/// encodes everything needed to add the correct import statement to a consumer
9/// file (e.g. a test file that declares the fixture as a parameter).
10#[derive(Debug, Clone, PartialEq)]
11pub struct TypeImportSpec {
12    /// The name to look for in the target file's module-level names set.
13    ///
14    /// Matches exactly how `collect_module_level_names` stores names:
15    /// - `import pathlib`                  → `"pathlib"`
16    /// - `from pathlib import Path`        → `"Path"`
17    /// - `from pathlib import Path as P`   → `"P"`
18    pub check_name: String,
19    /// Complete import statement to insert (no trailing newline).
20    /// Always in absolute form — relative imports are resolved at analysis time.
21    ///
22    /// Examples: `"from pathlib import Path"`, `"import pathlib"`,
23    ///           `"from pathlib import Path as P"`
24    pub import_statement: String,
25}
26
27/// Pytest fixture scope, ordered from narrowest to broadest.
28/// A fixture with a broader scope cannot depend on a fixture with a narrower scope.
29#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
30pub enum FixtureScope {
31    /// Function scope (default) - created once per test function
32    #[default]
33    Function = 0,
34    /// Class scope - created once per test class
35    Class = 1,
36    /// Module scope - created once per test module
37    Module = 2,
38    /// Package scope - created once per test package
39    Package = 3,
40    /// Session scope - created once per test session
41    Session = 4,
42}
43
44impl FixtureScope {
45    /// Parse scope from a string (as used in @pytest.fixture(scope="..."))
46    pub fn parse(s: &str) -> Option<Self> {
47        match s.to_lowercase().as_str() {
48            "function" => Some(Self::Function),
49            "class" => Some(Self::Class),
50            "module" => Some(Self::Module),
51            "package" => Some(Self::Package),
52            "session" => Some(Self::Session),
53            _ => None,
54        }
55    }
56
57    /// Get display name for the scope
58    pub fn as_str(&self) -> &'static str {
59        match self {
60            Self::Function => "function",
61            Self::Class => "class",
62            Self::Module => "module",
63            Self::Package => "package",
64            Self::Session => "session",
65        }
66    }
67}
68
69/// A fixture definition extracted from a Python file.
70///
71/// New fields may be added in future versions.  External crates constructing
72/// literals should use the struct-update syntax to stay forward-compatible:
73///
74/// ```rust,ignore
75/// let def = FixtureDefinition {
76///     name: "my_fixture".to_string(),
77///     file_path: PathBuf::from("/tmp/conftest.py"),
78///     ..Default::default()
79/// };
80/// ```
81#[derive(Debug, Clone, Default, PartialEq)]
82pub struct FixtureDefinition {
83    pub name: String,
84    pub file_path: PathBuf,
85    pub line: usize,
86    pub end_line: usize, // Line number where the function ends (for document symbol ranges)
87    pub start_char: usize, // Character position where the fixture name starts (on the line)
88    pub end_char: usize, // Character position where the fixture name ends (on the line)
89    pub docstring: Option<String>,
90    pub return_type: Option<String>, // The return type annotation (for generators, the yielded type)
91    pub return_type_imports: Vec<TypeImportSpec>, // Import specs needed to use the return type in another file
92    pub is_third_party: bool, // Whether this fixture is from a third-party package (site-packages)
93    pub is_plugin: bool, // Whether this fixture was discovered via a pytest11 entry point plugin
94    pub dependencies: Vec<String>, // Names of fixtures this fixture depends on (via parameters)
95    pub scope: FixtureScope, // The fixture's scope (function, class, module, package, session)
96    pub yield_line: Option<usize>, // Line number of the yield statement (for generator fixtures)
97    pub autouse: bool,   // Whether this fixture has autouse=True
98}
99
100/// A fixture usage (reference) in a Python file.
101///
102/// This struct is `#[non_exhaustive]`: new fields may be added in future versions
103/// without a semver-major bump.
104#[non_exhaustive]
105#[derive(Debug, Clone)]
106pub struct FixtureUsage {
107    pub name: String,
108    pub file_path: PathBuf,
109    pub line: usize,
110    pub start_char: usize, // Character position where this usage starts (on the line)
111    pub end_char: usize,   // Character position where this usage ends (on the line)
112    /// `true` when this usage is a function parameter that can receive a type annotation.
113    /// `false` for string-based usages inside `@pytest.mark.usefixtures(...)`,
114    /// `pytestmark = pytest.mark.usefixtures(...)`, or `@pytest.mark.parametrize(..., indirect=...)`.
115    pub is_parameter: bool,
116}
117
118/// An undeclared fixture used in a function body without being declared as a parameter.
119#[derive(Debug, Clone)]
120#[allow(dead_code)] // Fields used for debugging and future features
121pub struct UndeclaredFixture {
122    pub name: String,
123    pub file_path: PathBuf,
124    pub line: usize,
125    pub start_char: usize,
126    pub end_char: usize,
127    pub function_name: String, // Name of the test/fixture function where this is used
128    pub function_line: usize,  // Line where the function is defined
129}
130
131/// A circular dependency between fixtures.
132#[derive(Debug, Clone)]
133pub struct FixtureCycle {
134    /// The chain of fixtures forming the cycle (e.g., ["A", "B", "C", "A"]).
135    pub cycle_path: Vec<String>,
136    /// The fixture where the cycle was detected (first fixture in the cycle).
137    pub fixture: FixtureDefinition,
138}
139
140/// A scope mismatch where a broader-scoped fixture depends on a narrower-scoped fixture.
141#[derive(Debug, Clone)]
142pub struct ScopeMismatch {
143    /// The fixture with broader scope that has the invalid dependency.
144    pub fixture: FixtureDefinition,
145    /// The dependency fixture with narrower scope.
146    pub dependency: FixtureDefinition,
147}
148
149/// Context for code completion.
150#[derive(Debug, Clone, PartialEq)]
151pub enum CompletionContext {
152    /// Inside a function signature (parameter list) - suggest fixtures as parameters.
153    FunctionSignature {
154        function_name: String,
155        function_line: usize,
156        is_fixture: bool,
157        declared_params: Vec<String>,
158        /// The fixture's scope if inside a fixture function, None for test functions.
159        fixture_scope: Option<FixtureScope>,
160    },
161    /// Inside a function body - suggest fixtures with auto-add to parameters.
162    FunctionBody {
163        function_name: String,
164        function_line: usize,
165        is_fixture: bool,
166        declared_params: Vec<String>,
167        /// The fixture's scope if inside a fixture function, None for test functions.
168        fixture_scope: Option<FixtureScope>,
169    },
170    /// Inside @pytest.mark.usefixtures("...") decorator - suggest fixture names as strings.
171    UsefixturesDecorator,
172    /// Inside @pytest.mark.parametrize(..., indirect=...) - suggest fixture names as strings.
173    ParametrizeIndirect,
174}
175
176/// Information about where to insert a new parameter in a function signature.
177#[derive(Debug, Clone, PartialEq)]
178pub struct ParamInsertionInfo {
179    /// Line number (1-indexed) where the new parameter should be inserted.
180    pub line: usize,
181    /// Character position where the new parameter should be inserted.
182    pub char_pos: usize,
183    /// Whether a comma needs to be added before the new parameter.
184    ///
185    /// For single-line signatures: prepend `, ` to the new parameter text.
186    /// For multiline signatures (`multiline_indent` is `Some`): if `true`, the
187    /// last argument has no trailing comma and one must be appended there; if
188    /// `false`, a trailing comma already exists and only the new-line + indent
189    /// prefix is needed.
190    pub needs_comma: bool,
191    /// For multiline signatures where `)` sits on its own line: the indentation
192    /// string (spaces/tabs) to use for the new parameter.  The insertion point
193    /// (`line` / `char_pos`) is set to right after the last argument's content
194    /// rather than at the `)` itself.
195    ///
196    /// When `Some`, callers should produce:
197    ///   - `needs_comma=true`  → insert `,\n<indent><param>`  (adds trailing comma to prev arg)
198    ///   - `needs_comma=false` → insert `\n<indent><param>,`  (mirrors trailing-comma style)
199    ///
200    /// When `None` this is a single-line (or inline-paren) signature and the
201    /// classic `, <param>` / `<param>` text applies.
202    pub multiline_indent: Option<String>,
203}