diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 1fbb4f78fe..b38af20ee5 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -31,7 +31,7 @@ use ruff_python_parser::semantic_errors::{ }; use rustc_hash::{FxHashMap, FxHashSet}; -use ruff_diagnostics::{Diagnostic, IsolationLevel}; +use ruff_diagnostics::{Diagnostic, Edit, IsolationLevel}; use ruff_notebook::{CellOffsets, NotebookIndex}; use ruff_python_ast::helpers::{collect_import_from_member, is_docstring_stmt, to_module_path}; use ruff_python_ast::identifier::Identifier; @@ -62,7 +62,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::annotation::AnnotationContext; use crate::docstrings::extraction::ExtractionTarget; -use crate::importer::Importer; +use crate::importer::{ImportRequest, Importer, ResolutionError}; use crate::noqa::NoqaMapping; use crate::package::PackageRoot; use crate::registry::Rule; @@ -530,6 +530,28 @@ impl<'a> Checker<'a> { f(&mut checker, self); self.semantic_checker = checker; } + + /// Attempt to create an [`Edit`] that imports `member`. + /// + /// On Python <`version_added_to_typing`, `member` is imported from `typing_extensions`, while + /// on Python >=`version_added_to_typing`, it is imported from `typing`. + /// + /// See [`Importer::get_or_import_symbol`] for more details on the returned values. + pub(crate) fn import_from_typing( + &self, + member: &str, + position: TextSize, + version_added_to_typing: PythonVersion, + ) -> Result<(Edit, String), ResolutionError> { + let source_module = if self.target_version() >= version_added_to_typing { + "typing" + } else { + "typing_extensions" + }; + let request = ImportRequest::import_from(source_module, member); + self.importer() + .get_or_import_symbol(&request, position, self.semantic()) + } } impl SemanticSyntaxContext for Checker<'_> { diff --git a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs index ec0e5e33a1..9e537d398c 100644 --- a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs +++ b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs @@ -6,7 +6,6 @@ use ruff_python_semantic::Modules; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; -use crate::importer::ImportRequest; use crate::rules::fastapi::rules::is_fastapi_route; use ruff_python_ast::PythonVersion; @@ -232,15 +231,10 @@ fn create_diagnostic( ); let try_generate_fix = || { - let module = if checker.target_version() >= PythonVersion::PY39 { - "typing" - } else { - "typing_extensions" - }; - let (import_edit, binding) = checker.importer().get_or_import_symbol( - &ImportRequest::import_from(module, "Annotated"), + let (import_edit, binding) = checker.import_from_typing( + "Annotated", parameter.range.start(), - checker.semantic(), + PythonVersion::PY39, )?; // Each of these classes takes a single, optional default diff --git a/crates/ruff_linter/src/rules/flake8_annotations/helpers.rs b/crates/ruff_linter/src/rules/flake8_annotations/helpers.rs index 25efc7aa14..1adf672cfe 100644 --- a/crates/ruff_linter/src/rules/flake8_annotations/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_annotations/helpers.rs @@ -14,7 +14,7 @@ use ruff_python_semantic::analyze::visibility; use ruff_python_semantic::{Definition, SemanticModel}; use ruff_text_size::{TextRange, TextSize}; -use crate::importer::{ImportRequest, Importer}; +use crate::checkers::ast::Checker; use ruff_python_ast::PythonVersion; /// Return the name of the function, if it's overloaded. @@ -119,26 +119,19 @@ impl AutoPythonType { /// additional edits. pub(crate) fn into_expression( self, - importer: &Importer, + checker: &Checker, at: TextSize, - semantic: &SemanticModel, - target_version: PythonVersion, ) -> Option<(Expr, Vec)> { + let target_version = checker.target_version(); match self { AutoPythonType::Never => { - let (no_return_edit, binding) = importer - .get_or_import_symbol( - &ImportRequest::import_from( - "typing", - if target_version >= PythonVersion::PY311 { - "Never" - } else { - "NoReturn" - }, - ), - at, - semantic, - ) + let member = if target_version >= PythonVersion::PY311 { + "Never" + } else { + "NoReturn" + }; + let (no_return_edit, binding) = checker + .import_from_typing(member, at, PythonVersion::lowest()) .ok()?; let expr = Expr::Name(ast::ExprName { id: Name::from(binding), @@ -175,12 +168,8 @@ impl AutoPythonType { let element = type_expr(*python_type)?; // Ex) `Optional[int]` - let (optional_edit, binding) = importer - .get_or_import_symbol( - &ImportRequest::import_from("typing", "Optional"), - at, - semantic, - ) + let (optional_edit, binding) = checker + .import_from_typing("Optional", at, PythonVersion::lowest()) .ok()?; let expr = typing_optional(element, Name::from(binding)); Some((expr, vec![optional_edit])) @@ -192,12 +181,8 @@ impl AutoPythonType { .collect::>>()?; // Ex) `Union[int, str]` - let (union_edit, binding) = importer - .get_or_import_symbol( - &ImportRequest::import_from("typing", "Union"), - at, - semantic, - ) + let (union_edit, binding) = checker + .import_from_typing("Union", at, PythonVersion::lowest()) .ok()?; let expr = typing_union(&elements, Name::from(binding)); Some((expr, vec![union_edit])) diff --git a/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs index fe3f2da497..786877146c 100644 --- a/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs @@ -721,12 +721,7 @@ pub(crate) fn definition( } else { auto_return_type(function) .and_then(|return_type| { - return_type.into_expression( - checker.importer(), - function.parameters.start(), - checker.semantic(), - checker.target_version(), - ) + return_type.into_expression(checker, function.parameters.start()) }) .map(|(return_type, edits)| (checker.generator().expr(&return_type), edits)) }; @@ -752,12 +747,7 @@ pub(crate) fn definition( } else { auto_return_type(function) .and_then(|return_type| { - return_type.into_expression( - checker.importer(), - function.parameters.start(), - checker.semantic(), - checker.target_version(), - ) + return_type.into_expression(checker, function.parameters.start()) }) .map(|(return_type, edits)| (checker.generator().expr(&return_type), edits)) }; @@ -822,12 +812,8 @@ pub(crate) fn definition( } else { auto_return_type(function) .and_then(|return_type| { - return_type.into_expression( - checker.importer(), - function.parameters.start(), - checker.semantic(), - checker.target_version(), - ) + return_type + .into_expression(checker, function.parameters.start()) }) .map(|(return_type, edits)| { (checker.generator().expr(&return_type), edits) @@ -861,12 +847,8 @@ pub(crate) fn definition( } else { auto_return_type(function) .and_then(|return_type| { - return_type.into_expression( - checker.importer(), - function.parameters.start(), - checker.semantic(), - checker.target_version(), - ) + return_type + .into_expression(checker, function.parameters.start()) }) .map(|(return_type, edits)| { (checker.generator().expr(&return_type), edits) diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs index 84546f130a..e3b84a605a 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs @@ -8,10 +8,9 @@ use ruff_python_semantic::analyze::class::is_metaclass; use ruff_python_semantic::analyze::function_type::{self, FunctionType}; use ruff_python_semantic::analyze::visibility::{is_abstract, is_overload}; use ruff_python_semantic::{Binding, ResolvedReference, ScopeId, SemanticModel}; -use ruff_text_size::{Ranged, TextRange, TextSize}; +use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; -use crate::importer::{ImportRequest, ResolutionError}; use ruff_python_ast::PythonVersion; /// ## What it does @@ -317,7 +316,8 @@ fn replace_custom_typevar_with_self( self_or_cls_annotation: &ast::Expr, ) -> anyhow::Result { // (1) Import `Self` (if necessary) - let (import_edit, self_symbol_binding) = import_self(checker, function_def.start())?; + let (import_edit, self_symbol_binding) = + checker.import_from_typing("Self", function_def.start(), PythonVersion::PY311)?; // (2) Remove the first parameter's annotation let mut other_edits = vec![Edit::deletion( @@ -367,24 +367,6 @@ fn replace_custom_typevar_with_self( )) } -/// Attempt to create an [`Edit`] that imports `Self`. -/// -/// On Python <3.11, `Self` is imported from `typing_extensions`; -/// on Python >=3.11, it is imported from `typing`. -/// This is because it was added to the `typing` module on Python 3.11, -/// but is available from the backport package `typing_extensions` on all versions. -fn import_self(checker: &Checker, position: TextSize) -> Result<(Edit, String), ResolutionError> { - let source_module = if checker.target_version() >= PythonVersion::PY311 { - "typing" - } else { - "typing_extensions" - }; - let request = ImportRequest::import_from(source_module, "Self"); - checker - .importer() - .get_or_import_symbol(&request, position, checker.semantic()) -} - /// Returns a series of [`Edit`]s that modify all references to the given `typevar`. /// /// Only references within `editable_range` will be modified. diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_union_member.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_union_member.rs index f4e02b222f..2ddc53ba1e 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_union_member.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_union_member.rs @@ -2,18 +2,19 @@ use std::collections::HashSet; use anyhow::Result; -use ruff_python_ast::name::Name; use rustc_hash::FxHashSet; use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::comparable::ComparableExpr; -use ruff_python_ast::{Expr, ExprBinOp, ExprContext, ExprName, ExprSubscript, ExprTuple, Operator}; +use ruff_python_ast::name::Name; +use ruff_python_ast::{ + Expr, ExprBinOp, ExprContext, ExprName, ExprSubscript, ExprTuple, Operator, PythonVersion, +}; use ruff_python_semantic::analyze::typing::traverse_union; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; -use crate::importer::ImportRequest; /// ## What it does /// Checks for duplicate union members. @@ -181,11 +182,8 @@ fn generate_union_fix( debug_assert!(nodes.len() >= 2, "At least two nodes required"); // Request `typing.Union` - let (import_edit, binding) = checker.importer().get_or_import_symbol( - &ImportRequest::import_from("typing", "Union"), - annotation.start(), - checker.semantic(), - )?; + let (import_edit, binding) = + checker.import_from_typing("Union", annotation.start(), PythonVersion::lowest())?; // Construct the expression as `Subscript[typing.Union, Tuple[expr, [expr, ...]]]` let new_expr = Expr::Subscript(ExprSubscript { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs index 1675922077..1eda92b7ee 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs @@ -1,5 +1,4 @@ use crate::checkers::ast::Checker; -use crate::importer::ImportRequest; use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast as ast; @@ -214,17 +213,8 @@ fn replace_with_self_fix( ) -> anyhow::Result { let semantic = checker.semantic(); - let (self_import, self_binding) = { - let source_module = if checker.target_version() >= PythonVersion::PY311 { - "typing" - } else { - "typing_extensions" - }; - - let (importer, semantic) = (checker.importer(), checker.semantic()); - let request = ImportRequest::import_from(source_module, "Self"); - importer.get_or_import_symbol(&request, returns.start(), semantic)? - }; + let (self_import, self_binding) = + checker.import_from_typing("Self", returns.start(), PythonVersion::PY311)?; let mut others = Vec::with_capacity(2); diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs index 8fa05574a0..16a48bc5f5 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs @@ -12,7 +12,7 @@ use ruff_text_size::{Ranged, TextRange}; use smallvec::SmallVec; -use crate::{checkers::ast::Checker, importer::ImportRequest}; +use crate::checkers::ast::Checker; /// ## What it does /// Checks for redundant `Literal[None]` annotations. @@ -225,10 +225,10 @@ fn create_fix( let fix = match union_kind { UnionKind::TypingOptional => { - let (import_edit, bound_name) = checker.importer().get_or_import_symbol( - &ImportRequest::import_from("typing", "Optional"), + let (import_edit, bound_name) = checker.import_from_typing( + "Optional", literal_expr.start(), - checker.semantic(), + PythonVersion::lowest(), )?; let optional_expr = typing_optional(new_literal_expr, Name::from(bound_name)); let content = checker.generator().expr(&optional_expr); diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs index 494203d461..2f196c2caf 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs @@ -6,12 +6,12 @@ use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Vi use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::{ name::Name, AnyParameterRef, Expr, ExprBinOp, ExprContext, ExprName, ExprSubscript, ExprTuple, - Operator, Parameters, + Operator, Parameters, PythonVersion, }; use ruff_python_semantic::analyze::typing::traverse_union; use ruff_text_size::{Ranged, TextRange}; -use crate::{checkers::ast::Checker, importer::ImportRequest}; +use crate::checkers::ast::Checker; /// ## What it does /// Checks for parameter annotations that contain redundant unions between @@ -268,11 +268,8 @@ fn generate_union_fix( debug_assert!(nodes.len() >= 2, "At least two nodes required"); // Request `typing.Union` - let (import_edit, binding) = checker.importer().get_or_import_symbol( - &ImportRequest::import_from("typing", "Union"), - annotation.start(), - checker.semantic(), - )?; + let (import_edit, binding) = + checker.import_from_typing("Optional", annotation.start(), PythonVersion::lowest())?; // Construct the expression as `Subscript[typing.Union, Tuple[expr, [expr, ...]]]` let new_expr = Expr::Subscript(ExprSubscript { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs index 2e8035af07..3a2c05e4d1 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs @@ -6,7 +6,6 @@ use ruff_python_semantic::{analyze::class::is_enumeration, ScopeKind, SemanticMo use ruff_text_size::Ranged; use crate::checkers::ast::Checker; -use crate::importer::ImportRequest; use crate::rules::flake8_pyi::rules::TypingModule; use crate::Locator; use ruff_python_ast::PythonVersion; @@ -682,11 +681,8 @@ pub(crate) fn type_alias_without_annotation(checker: &Checker, value: &Expr, tar target.range(), ); diagnostic.try_set_fix(|| { - let (import_edit, binding) = checker.importer().get_or_import_symbol( - &ImportRequest::import(module.as_str(), "TypeAlias"), - target.start(), - checker.semantic(), - )?; + let (import_edit, binding) = + checker.import_from_typing("TypeAlias", target.start(), PythonVersion::PY310)?; Ok(Fix::safe_edits( Edit::range_replacement(format!("{id}: {binding}"), target.range()), [import_edit], diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI026_PYI026.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI026_PYI026.pyi.snap index 17bb45aedd..405375d032 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI026_PYI026.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI026_PYI026.pyi.snap @@ -14,10 +14,10 @@ PYI026.pyi:3:1: PYI026 [*] Use `typing_extensions.TypeAlias` for type alias, e.g ℹ Safe fix 1 1 | from typing import Literal, Any - 2 |+import typing_extensions + 2 |+from typing_extensions import TypeAlias 2 3 | 3 |-NewAny = Any - 4 |+NewAny: typing_extensions.TypeAlias = Any + 4 |+NewAny: TypeAlias = Any 4 5 | OptionalStr = typing.Optional[str] 5 6 | Foo = Literal["foo"] 6 7 | IntOrStr = int | str @@ -34,11 +34,11 @@ PYI026.pyi:4:1: PYI026 [*] Use `typing_extensions.TypeAlias` for type alias, e.g ℹ Safe fix 1 1 | from typing import Literal, Any - 2 |+import typing_extensions + 2 |+from typing_extensions import TypeAlias 2 3 | 3 4 | NewAny = Any 4 |-OptionalStr = typing.Optional[str] - 5 |+OptionalStr: typing_extensions.TypeAlias = typing.Optional[str] + 5 |+OptionalStr: TypeAlias = typing.Optional[str] 5 6 | Foo = Literal["foo"] 6 7 | IntOrStr = int | str 7 8 | AliasNone = None @@ -56,12 +56,12 @@ PYI026.pyi:5:1: PYI026 [*] Use `typing_extensions.TypeAlias` for type alias, e.g ℹ Safe fix 1 1 | from typing import Literal, Any - 2 |+import typing_extensions + 2 |+from typing_extensions import TypeAlias 2 3 | 3 4 | NewAny = Any 4 5 | OptionalStr = typing.Optional[str] 5 |-Foo = Literal["foo"] - 6 |+Foo: typing_extensions.TypeAlias = Literal["foo"] + 6 |+Foo: TypeAlias = Literal["foo"] 6 7 | IntOrStr = int | str 7 8 | AliasNone = None 8 9 | @@ -78,13 +78,13 @@ PYI026.pyi:6:1: PYI026 [*] Use `typing_extensions.TypeAlias` for type alias, e.g ℹ Safe fix 1 1 | from typing import Literal, Any - 2 |+import typing_extensions + 2 |+from typing_extensions import TypeAlias 2 3 | 3 4 | NewAny = Any 4 5 | OptionalStr = typing.Optional[str] 5 6 | Foo = Literal["foo"] 6 |-IntOrStr = int | str - 7 |+IntOrStr: typing_extensions.TypeAlias = int | str + 7 |+IntOrStr: TypeAlias = int | str 7 8 | AliasNone = None 8 9 | 9 10 | NewAny: typing.TypeAlias = Any @@ -102,14 +102,14 @@ PYI026.pyi:7:1: PYI026 [*] Use `typing_extensions.TypeAlias` for type alias, e.g ℹ Safe fix 1 1 | from typing import Literal, Any - 2 |+import typing_extensions + 2 |+from typing_extensions import TypeAlias 2 3 | 3 4 | NewAny = Any 4 5 | OptionalStr = typing.Optional[str] 5 6 | Foo = Literal["foo"] 6 7 | IntOrStr = int | str 7 |-AliasNone = None - 8 |+AliasNone: typing_extensions.TypeAlias = None + 8 |+AliasNone: TypeAlias = None 8 9 | 9 10 | NewAny: typing.TypeAlias = Any 10 11 | OptionalStr: TypeAlias = typing.Optional[str] @@ -126,7 +126,7 @@ PYI026.pyi:17:5: PYI026 [*] Use `typing_extensions.TypeAlias` for type alias, e. ℹ Safe fix 1 1 | from typing import Literal, Any - 2 |+import typing_extensions + 2 |+from typing_extensions import TypeAlias 2 3 | 3 4 | NewAny = Any 4 5 | OptionalStr = typing.Optional[str] @@ -135,7 +135,7 @@ PYI026.pyi:17:5: PYI026 [*] Use `typing_extensions.TypeAlias` for type alias, e. 15 16 | 16 17 | class NotAnEnum: 17 |- FLAG_THIS = None - 18 |+ FLAG_THIS: typing_extensions.TypeAlias = None + 18 |+ FLAG_THIS: TypeAlias = None 18 19 | 19 20 | # these are ok 20 21 | from enum import Enum diff --git a/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs index adaec2f99f..c59a94b1a1 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs @@ -10,7 +10,6 @@ use ruff_python_ast::{self as ast, Expr, Operator, Parameters}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; -use crate::importer::ImportRequest; use ruff_python_ast::PythonVersion; @@ -137,11 +136,8 @@ fn generate_fix(checker: &Checker, conversion_type: ConversionType, expr: &Expr) ))) } ConversionType::Optional => { - let (import_edit, binding) = checker.importer().get_or_import_symbol( - &ImportRequest::import_from("typing", "Optional"), - expr.start(), - checker.semantic(), - )?; + let (import_edit, binding) = + checker.import_from_typing("Optional", expr.start(), PythonVersion::lowest())?; let new_expr = Expr::Subscript(ast::ExprSubscript { range: TextRange::default(), value: Box::new(Expr::Name(ast::ExprName { diff --git a/crates/ruff_python_ast/src/python_version.rs b/crates/ruff_python_ast/src/python_version.rs index cdbb9c8f3d..e5d1406ded 100644 --- a/crates/ruff_python_ast/src/python_version.rs +++ b/crates/ruff_python_ast/src/python_version.rs @@ -44,6 +44,11 @@ impl PythonVersion { .into_iter() } + /// The minimum supported Python version. + pub const fn lowest() -> Self { + Self::PY37 + } + pub const fn latest() -> Self { Self::PY313 }