Skip to content

Commit 5ec7c13

Browse files
authored
[ruff-0.10] [flake8-pyi] Stabilize preview-mode behaviours for custom-type-var-for-self(PYI019) (#16607)
## Summary This PR stabilizes several preview-only behaviours for `custom-typevar-for-self` (`PYI019`). Namely: - A new, more accurate technique is now employed for detecting custom TypeVars that are replaceable with `Self`. See #15888 for details. - The range of the diagnostic is now the full function header rather than just the return annotation. (Previously, the rule only applied to methods with return annotations, but this is no longer true due to the changes in the first bullet point.) - The fix is now available even when preview mode is not enabled. ## Test Plan - Existing snapshots that do not have preview mode enabled are updated - Preview-specific snapshots are removed - I'll check the ecosystem report on this PR to verify everything's as expected
1 parent 303b061 commit 5ec7c13

8 files changed

+954
-1783
lines changed

crates/ruff_linter/src/rules/flake8_pyi/mod.rs

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -156,30 +156,6 @@ mod tests {
156156
Ok(())
157157
}
158158

159-
#[test_case(Rule::CustomTypeVarForSelf, Path::new("PYI019_0.py"))]
160-
#[test_case(Rule::CustomTypeVarForSelf, Path::new("PYI019_0.pyi"))]
161-
#[test_case(Rule::CustomTypeVarForSelf, Path::new("PYI019_1.pyi"))]
162-
fn custom_classmethod_rules_preview(rule_code: Rule, path: &Path) -> Result<()> {
163-
let snapshot = format!(
164-
"preview_{}_{}",
165-
rule_code.noqa_code(),
166-
path.to_string_lossy()
167-
);
168-
let diagnostics = test_path(
169-
Path::new("flake8_pyi").join(path).as_path(),
170-
&settings::LinterSettings {
171-
pep8_naming: pep8_naming::settings::Settings {
172-
classmethod_decorators: vec!["foo_classmethod".to_string()],
173-
..pep8_naming::settings::Settings::default()
174-
},
175-
preview: PreviewMode::Enabled,
176-
..settings::LinterSettings::for_rule(rule_code)
177-
},
178-
)?;
179-
assert_messages!(snapshot, diagnostics);
180-
Ok(())
181-
}
182-
183159
#[test_case(Rule::TypeAliasWithoutAnnotation, Path::new("PYI026.py"))]
184160
#[test_case(Rule::TypeAliasWithoutAnnotation, Path::new("PYI026.pyi"))]
185161
#[test_case(Rule::RedundantNoneLiteral, Path::new("PYI061.py"))]

crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs

Lines changed: 26 additions & 212 deletions
Original file line numberDiff line numberDiff line change
@@ -71,20 +71,8 @@ use ruff_python_ast::PythonVersion;
7171
/// The fix is only marked as unsafe if there is the possibility that it might delete a comment
7272
/// from your code.
7373
///
74-
/// ## Preview-mode behaviour
75-
/// This rule's behaviour has several differences when [`preview`] mode is enabled:
76-
/// 1. The fix for this rule is currently only available if `preview` mode is enabled.
77-
/// 2. By default, this rule is only applied to methods that have return-type annotations,
78-
/// and the range of the diagnostic is the range of the return-type annotation.
79-
/// In preview mode, this rule is also applied to some methods that do not have
80-
/// return-type annotations. The range of the diagnostic is the range of the function
81-
/// header (from the end of the function name to the end of the parameters).
82-
/// 3. In `preview` mode, the rule uses different logic to determine whether an annotation
83-
/// refers to a type variable. The `preview`-mode logic is more accurate, but may lead
84-
/// to more methods being flagged than if `preview` mode is disabled.
85-
///
8674
/// [PEP 673]: https://peps.python.org/pep-0673/#motivation
87-
/// [PEP 695]: https://peps.python.org/pep-0695/
75+
/// [PEP-695]: https://peps.python.org/pep-0695/
8876
/// [PYI018]: https://docs.astral.sh/ruff/rules/unused-private-type-var/
8977
/// [type parameter list]: https://docs.python.org/3/reference/compound_stmts.html#type-params
9078
/// [Self]: https://docs.python.org/3/library/typing.html#typing.Self
@@ -162,73 +150,33 @@ pub(crate) fn custom_type_var_instead_of_self(
162150
&checker.settings.pep8_naming.staticmethod_decorators,
163151
);
164152

165-
let function_header_end = returns
166-
.as_deref()
167-
.map(Ranged::end)
168-
.unwrap_or_else(|| parameters.end());
169-
170-
// In stable mode, we only emit the diagnostic on methods that have a return type annotation.
171-
// In preview mode, we have a more principled approach to determine if an annotation refers
172-
// to a type variable, and we emit the diagnostic on some methods that do not have return
173-
// annotations.
174-
let (method, diagnostic_range) = match function_kind {
175-
FunctionType::ClassMethod | FunctionType::NewMethod => {
176-
if checker.settings.preview.is_enabled() {
177-
(
178-
Method::PreviewClass(PreviewClassMethod {
179-
cls_annotation: self_or_cls_annotation,
180-
type_params,
181-
}),
182-
TextRange::new(function_name.end(), function_header_end),
183-
)
184-
} else {
185-
returns.as_deref().map(|returns| {
186-
(
187-
Method::Class(ClassMethod {
188-
cls_annotation: self_or_cls_annotation,
189-
returns,
190-
type_params,
191-
}),
192-
returns.range(),
193-
)
194-
})?
195-
}
196-
}
197-
FunctionType::Method => {
198-
if checker.settings.preview.is_enabled() {
199-
(
200-
Method::PreviewInstance(PreviewInstanceMethod {
201-
self_annotation: self_or_cls_annotation,
202-
type_params,
203-
}),
204-
TextRange::new(function_name.end(), function_header_end),
205-
)
206-
} else {
207-
returns.as_deref().map(|returns| {
208-
(
209-
Method::Instance(InstanceMethod {
210-
self_annotation: self_or_cls_annotation,
211-
returns,
212-
type_params,
213-
}),
214-
returns.range(),
215-
)
216-
})?
217-
}
218-
}
153+
let method = match function_kind {
154+
FunctionType::ClassMethod | FunctionType::NewMethod => Method::Class(ClassMethod {
155+
cls_annotation: self_or_cls_annotation,
156+
type_params,
157+
}),
158+
FunctionType::Method => Method::Instance(InstanceMethod {
159+
self_annotation: self_or_cls_annotation,
160+
type_params,
161+
}),
219162
FunctionType::Function | FunctionType::StaticMethod => return None,
220163
};
221164

222165
let custom_typevar = method.custom_typevar(semantic, binding.scope)?;
223166

167+
let function_header_end = returns
168+
.as_deref()
169+
.map(Ranged::end)
170+
.unwrap_or_else(|| parameters.end());
171+
224172
let mut diagnostic = Diagnostic::new(
225173
CustomTypeVarForSelf {
226174
typevar_name: custom_typevar.name(checker.source()).to_string(),
227175
},
228-
diagnostic_range,
176+
TextRange::new(function_name.end(), function_header_end),
229177
);
230178

231-
diagnostic.try_set_optional_fix(|| {
179+
diagnostic.try_set_fix(|| {
232180
replace_custom_typevar_with_self(
233181
checker,
234182
function_def,
@@ -244,9 +192,7 @@ pub(crate) fn custom_type_var_instead_of_self(
244192
#[derive(Debug)]
245193
enum Method<'a> {
246194
Class(ClassMethod<'a>),
247-
PreviewClass(PreviewClassMethod<'a>),
248195
Instance(InstanceMethod<'a>),
249-
PreviewInstance(PreviewInstanceMethod<'a>),
250196
}
251197

252198
impl Method<'_> {
@@ -257,86 +203,18 @@ impl Method<'_> {
257203
) -> Option<TypeVar<'a>> {
258204
match self {
259205
Self::Class(class_method) => class_method.custom_typevar(semantic, scope),
260-
Self::PreviewClass(class_method) => class_method.custom_typevar(semantic, scope),
261206
Self::Instance(instance_method) => instance_method.custom_typevar(semantic),
262-
Self::PreviewInstance(instance_method) => instance_method.custom_typevar(semantic),
263207
}
264208
}
265209
}
266210

267211
#[derive(Debug)]
268212
struct ClassMethod<'a> {
269213
cls_annotation: &'a ast::Expr,
270-
returns: &'a ast::Expr,
271214
type_params: Option<&'a ast::TypeParams>,
272215
}
273216

274217
impl ClassMethod<'_> {
275-
/// Returns `Some(typevar)` if the class method is annotated with
276-
/// a custom `TypeVar` that is likely private.
277-
fn custom_typevar<'a>(
278-
&'a self,
279-
semantic: &'a SemanticModel<'a>,
280-
scope: ScopeId,
281-
) -> Option<TypeVar<'a>> {
282-
let ast::ExprSubscript {
283-
value: cls_annotation_value,
284-
slice: cls_annotation_typevar,
285-
..
286-
} = self.cls_annotation.as_subscript_expr()?;
287-
288-
let cls_annotation_typevar = cls_annotation_typevar.as_name_expr()?;
289-
let cls_annotation_typevar_name = &cls_annotation_typevar.id;
290-
let ast::ExprName { id, .. } = cls_annotation_value.as_name_expr()?;
291-
292-
if id != "type" {
293-
return None;
294-
}
295-
296-
if !semantic.has_builtin_binding_in_scope("type", scope) {
297-
return None;
298-
}
299-
300-
let return_annotation_typevar = match self.returns {
301-
ast::Expr::Name(ast::ExprName { id, .. }) => id,
302-
ast::Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
303-
let return_annotation_typevar = slice.as_name_expr()?;
304-
let ast::ExprName { id, .. } = value.as_name_expr()?;
305-
if id != "type" {
306-
return None;
307-
}
308-
&return_annotation_typevar.id
309-
}
310-
_ => return None,
311-
};
312-
313-
if cls_annotation_typevar_name != return_annotation_typevar {
314-
return None;
315-
}
316-
317-
if !is_likely_private_typevar(cls_annotation_typevar_name, self.type_params) {
318-
return None;
319-
}
320-
321-
semantic
322-
.resolve_name(cls_annotation_typevar)
323-
.map(|binding_id| TypeVar(semantic.binding(binding_id)))
324-
}
325-
}
326-
327-
/// Struct for implementing this rule as applied to classmethods in preview mode.
328-
///
329-
/// In stable mode, we only emit this diagnostic on methods that have return annotations,
330-
/// so the stable-mode version of this struct has a `returns: &ast::Expr` field. In preview
331-
/// mode, we also emit this diagnostic on methods that do not have return annotations, so
332-
/// the preview-mode version of this struct does not have a `returns` field.
333-
#[derive(Debug)]
334-
struct PreviewClassMethod<'a> {
335-
cls_annotation: &'a ast::Expr,
336-
type_params: Option<&'a ast::TypeParams>,
337-
}
338-
339-
impl PreviewClassMethod<'_> {
340218
/// Returns `Some(typevar)` if the class method is annotated with
341219
/// a custom `TypeVar` for the `cls` parameter
342220
fn custom_typevar<'a>(
@@ -360,90 +238,30 @@ impl PreviewClassMethod<'_> {
360238
return None;
361239
}
362240

363-
custom_typevar_preview(cls_annotation_typevar, self.type_params, semantic)
241+
custom_typevar(cls_annotation_typevar, self.type_params, semantic)
364242
}
365243
}
366244

367245
#[derive(Debug)]
368246
struct InstanceMethod<'a> {
369247
self_annotation: &'a ast::Expr,
370-
returns: &'a ast::Expr,
371248
type_params: Option<&'a ast::TypeParams>,
372249
}
373250

374251
impl InstanceMethod<'_> {
375-
/// Returns `Some(typevar)` if the instance method is annotated with
376-
/// a custom `TypeVar` that is likely private.
377-
fn custom_typevar<'a>(&'a self, semantic: &'a SemanticModel<'a>) -> Option<TypeVar<'a>> {
378-
let self_annotation = self.self_annotation.as_name_expr()?;
379-
let first_arg_type = &self_annotation.id;
380-
381-
let ast::ExprName {
382-
id: return_type, ..
383-
} = self.returns.as_name_expr()?;
384-
385-
if first_arg_type != return_type {
386-
return None;
387-
}
388-
389-
if !is_likely_private_typevar(first_arg_type, self.type_params) {
390-
return None;
391-
}
392-
393-
semantic
394-
.resolve_name(self_annotation)
395-
.map(|binding_id| TypeVar(semantic.binding(binding_id)))
396-
}
397-
}
398-
399-
/// Struct for implementing this rule as applied to instance methods in preview mode.
400-
///
401-
/// In stable mode, we only emit this diagnostic on methods that have return annotations,
402-
/// so the stable-mode version of this struct has a `returns: &ast::Expr` field. In preview
403-
/// mode, we also emit this diagnostic on methods that do not have return annotations, so
404-
/// the preview-mode version of this struct does not have a `returns` field.
405-
#[derive(Debug)]
406-
struct PreviewInstanceMethod<'a> {
407-
self_annotation: &'a ast::Expr,
408-
type_params: Option<&'a ast::TypeParams>,
409-
}
410-
411-
impl PreviewInstanceMethod<'_> {
412252
/// Returns `Some(typevar)` if the instance method is annotated with
413253
/// a custom `TypeVar` for the `self` parameter
414254
fn custom_typevar<'a>(&'a self, semantic: &'a SemanticModel<'a>) -> Option<TypeVar<'a>> {
415-
custom_typevar_preview(
255+
custom_typevar(
416256
self.self_annotation.as_name_expr()?,
417257
self.type_params,
418258
semantic,
419259
)
420260
}
421261
}
422262

423-
/// Returns `true` if the type variable is likely private.
424-
///
425-
/// This routine is only used if `--preview` is not enabled,
426-
/// as it uses heuristics to determine if an annotation uses a type variable.
427-
/// In preview mode, we apply a more principled approach.
428-
fn is_likely_private_typevar(type_var_name: &str, type_params: Option<&ast::TypeParams>) -> bool {
429-
// Ex) `_T`
430-
if type_var_name.starts_with('_') {
431-
return true;
432-
}
433-
// Ex) `class Foo[T]: ...`
434-
type_params.is_some_and(|type_params| {
435-
type_params.iter().any(|type_param| {
436-
if let ast::TypeParam::TypeVar(ast::TypeParamTypeVar { name, .. }) = type_param {
437-
name == type_var_name
438-
} else {
439-
false
440-
}
441-
})
442-
})
443-
}
444-
445263
/// Returns `Some(TypeVar)` if `typevar_expr` refers to a `TypeVar` binding
446-
fn custom_typevar_preview<'a>(
264+
fn custom_typevar<'a>(
447265
typevar_expr: &'a ast::ExprName,
448266
type_params: Option<&ast::TypeParams>,
449267
semantic: &'a SemanticModel<'a>,
@@ -497,11 +315,7 @@ fn replace_custom_typevar_with_self(
497315
custom_typevar: TypeVar,
498316
self_or_cls_parameter: &ast::ParameterWithDefault,
499317
self_or_cls_annotation: &ast::Expr,
500-
) -> anyhow::Result<Option<Fix>> {
501-
if checker.settings.preview.is_disabled() {
502-
return Ok(None);
503-
}
504-
318+
) -> anyhow::Result<Fix> {
505319
// (1) Import `Self` (if necessary)
506320
let (import_edit, self_symbol_binding) = import_self(checker, function_def.start())?;
507321

@@ -513,9 +327,9 @@ fn replace_custom_typevar_with_self(
513327

514328
// (3) If it was a PEP-695 type variable, remove that `TypeVar` from the PEP-695 type-parameter list
515329
if custom_typevar.is_pep695_typevar() {
516-
let Some(type_params) = function_def.type_params.as_deref() else {
517-
bail!("Should not be possible to have a type parameter without a type parameter list");
518-
};
330+
let type_params = function_def.type_params.as_deref().context(
331+
"Should not be possible to have a type parameter without a type parameter list",
332+
)?;
519333
let deletion_edit = remove_pep695_typevar_declaration(type_params, custom_typevar)
520334
.context("Failed to find a `TypeVar` in the type params that matches the binding")?;
521335
other_edits.push(deletion_edit);
@@ -546,11 +360,11 @@ fn replace_custom_typevar_with_self(
546360
Applicability::Safe
547361
};
548362

549-
Ok(Some(Fix::applicable_edits(
363+
Ok(Fix::applicable_edits(
550364
import_edit,
551365
other_edits,
552366
applicability,
553-
)))
367+
))
554368
}
555369

556370
/// Attempt to create an [`Edit`] that imports `Self`.

0 commit comments

Comments
 (0)