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}