mirror of https://github.com/astral-sh/ruff
[`flake8-type-checking`] Fixes `quote_type_expression` (#14634)
This commit is contained in:
parent
6fcbe8efb4
commit
8a7ba5d2df
|
|
@ -60,3 +60,23 @@ def f():
|
|||
| None,
|
||||
3.0
|
||||
)
|
||||
|
||||
|
||||
def f():
|
||||
# Regression test for #14554
|
||||
import typing
|
||||
typing.cast(M-())
|
||||
|
||||
|
||||
def f():
|
||||
# Simple case with Literal that should lead to nested quotes
|
||||
from typing import cast, Literal
|
||||
|
||||
cast(Literal["A"], 'A')
|
||||
|
||||
|
||||
def f():
|
||||
# Really complex case with nested forward references
|
||||
from typing import cast, Annotated, Literal
|
||||
|
||||
cast(list[Annotated["list['Literal[\"A\"]']", "Foo"]], ['A'])
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ def f():
|
|||
from typing import Literal
|
||||
from third_party import Type
|
||||
|
||||
def test_string_contains_opposite_quote_do_not_fix(self, type1: Type[Literal["'"]], type2: Type[Literal["\'"]]):
|
||||
def test_string_contains_opposite_quote(self, type1: Type[Literal["'"]], type2: Type[Literal["\'"]]):
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
use std::cmp::Reverse;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::helpers::{map_callable, map_subscript};
|
||||
use ruff_python_ast::name::QualifiedName;
|
||||
use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal};
|
||||
use ruff_python_ast::visitor::transformer::{walk_expr, Transformer};
|
||||
use ruff_python_ast::{self as ast, Decorator, Expr};
|
||||
use ruff_python_codegen::{Generator, Stylist};
|
||||
use ruff_python_parser::typing::parse_type_annotation;
|
||||
use ruff_python_semantic::{
|
||||
analyze, Binding, BindingKind, Modules, NodeId, ResolvedReference, ScopeKind, SemanticModel,
|
||||
};
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::rules::flake8_type_checking::settings::Settings;
|
||||
use crate::Locator;
|
||||
|
||||
/// Returns `true` if the [`ResolvedReference`] is in a typing-only context _or_ a runtime-evaluated
|
||||
/// context (with quoting enabled).
|
||||
|
|
@ -229,7 +229,8 @@ pub(crate) fn quote_annotation(
|
|||
node_id: NodeId,
|
||||
semantic: &SemanticModel,
|
||||
stylist: &Stylist,
|
||||
) -> Result<Edit> {
|
||||
locator: &Locator,
|
||||
) -> Edit {
|
||||
let expr = semantic.expression(node_id).expect("Expression not found");
|
||||
if let Some(parent_id) = semantic.parent_expression_id(node_id) {
|
||||
match semantic.expression(parent_id) {
|
||||
|
|
@ -238,7 +239,7 @@ pub(crate) fn quote_annotation(
|
|||
// If we're quoting the value of a subscript, we need to quote the entire
|
||||
// expression. For example, when quoting `DataFrame` in `DataFrame[int]`, we
|
||||
// should generate `"DataFrame[int]"`.
|
||||
return quote_annotation(parent_id, semantic, stylist);
|
||||
return quote_annotation(parent_id, semantic, stylist, locator);
|
||||
}
|
||||
}
|
||||
Some(Expr::Attribute(parent)) => {
|
||||
|
|
@ -246,7 +247,7 @@ pub(crate) fn quote_annotation(
|
|||
// If we're quoting the value of an attribute, we need to quote the entire
|
||||
// expression. For example, when quoting `DataFrame` in `pd.DataFrame`, we
|
||||
// should generate `"pd.DataFrame"`.
|
||||
return quote_annotation(parent_id, semantic, stylist);
|
||||
return quote_annotation(parent_id, semantic, stylist, locator);
|
||||
}
|
||||
}
|
||||
Some(Expr::Call(parent)) => {
|
||||
|
|
@ -254,7 +255,7 @@ pub(crate) fn quote_annotation(
|
|||
// If we're quoting the function of a call, we need to quote the entire
|
||||
// expression. For example, when quoting `DataFrame` in `DataFrame()`, we
|
||||
// should generate `"DataFrame()"`.
|
||||
return quote_annotation(parent_id, semantic, stylist);
|
||||
return quote_annotation(parent_id, semantic, stylist, locator);
|
||||
}
|
||||
}
|
||||
Some(Expr::BinOp(parent)) => {
|
||||
|
|
@ -262,14 +263,14 @@ pub(crate) fn quote_annotation(
|
|||
// If we're quoting the left or right side of a binary operation, we need to
|
||||
// quote the entire expression. For example, when quoting `DataFrame` in
|
||||
// `DataFrame | Series`, we should generate `"DataFrame | Series"`.
|
||||
return quote_annotation(parent_id, semantic, stylist);
|
||||
return quote_annotation(parent_id, semantic, stylist, locator);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
quote_type_expression(expr, semantic, stylist)
|
||||
quote_type_expression(expr, semantic, stylist, locator)
|
||||
}
|
||||
|
||||
/// Wrap a type expression in quotes.
|
||||
|
|
@ -284,17 +285,12 @@ pub(crate) fn quote_type_expression(
|
|||
expr: &Expr,
|
||||
semantic: &SemanticModel,
|
||||
stylist: &Stylist,
|
||||
) -> Result<Edit> {
|
||||
locator: &Locator,
|
||||
) -> Edit {
|
||||
// Quote the entire expression.
|
||||
let quote = stylist.quote();
|
||||
let mut quote_annotator = QuoteAnnotator::new(semantic, stylist);
|
||||
quote_annotator.visit_expr(expr);
|
||||
let annotation = quote_annotator.into_annotation()?;
|
||||
let quote_annotator = QuoteAnnotator::new(semantic, stylist, locator);
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
format!("{quote}{annotation}{quote}"),
|
||||
expr.range(),
|
||||
))
|
||||
Edit::range_replacement(quote_annotator.into_annotation(expr), expr.range())
|
||||
}
|
||||
|
||||
/// Filter out any [`Edit`]s that are completely contained by any other [`Edit`].
|
||||
|
|
@ -317,144 +313,90 @@ pub(crate) fn filter_contained(edits: Vec<Edit>) -> Vec<Edit> {
|
|||
filtered
|
||||
}
|
||||
|
||||
#[derive(Copy, PartialEq, Clone)]
|
||||
enum QuoteAnnotatorState {
|
||||
Literal,
|
||||
AnnotatedFirst,
|
||||
AnnotatedRest,
|
||||
Other,
|
||||
}
|
||||
|
||||
pub(crate) struct QuoteAnnotator<'a> {
|
||||
stylist: &'a Stylist<'a>,
|
||||
semantic: &'a SemanticModel<'a>,
|
||||
state: Vec<QuoteAnnotatorState>,
|
||||
annotation: String,
|
||||
cannot_fix: bool,
|
||||
stylist: &'a Stylist<'a>,
|
||||
locator: &'a Locator<'a>,
|
||||
}
|
||||
|
||||
impl<'a> QuoteAnnotator<'a> {
|
||||
fn new(semantic: &'a SemanticModel<'a>, stylist: &'a Stylist<'a>) -> Self {
|
||||
fn new(
|
||||
semantic: &'a SemanticModel<'a>,
|
||||
stylist: &'a Stylist<'a>,
|
||||
locator: &'a Locator<'a>,
|
||||
) -> Self {
|
||||
Self {
|
||||
stylist,
|
||||
semantic,
|
||||
state: Vec::new(),
|
||||
annotation: String::new(),
|
||||
cannot_fix: false,
|
||||
stylist,
|
||||
locator,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_annotation(self) -> Result<String> {
|
||||
if self.cannot_fix {
|
||||
Err(anyhow::anyhow!(
|
||||
"Cannot quote annotation because it already contains opposite quote or escape character"
|
||||
))
|
||||
} else {
|
||||
Ok(self.annotation)
|
||||
fn into_annotation(self, expr: &Expr) -> String {
|
||||
let mut expr_without_forward_references = expr.clone();
|
||||
self.visit_expr(&mut expr_without_forward_references);
|
||||
let generator = Generator::from(self.stylist);
|
||||
// we first generate the annotation with the inverse quote, so we can
|
||||
// generate the string literal with the preferred quote
|
||||
let subgenerator = Generator::new(
|
||||
self.stylist.indentation(),
|
||||
self.stylist.quote().opposite(),
|
||||
self.stylist.line_ending(),
|
||||
);
|
||||
let annotation = subgenerator.expr(&expr_without_forward_references);
|
||||
generator.expr(&Expr::from(ast::StringLiteral {
|
||||
range: TextRange::default(),
|
||||
value: annotation.into_boxed_str(),
|
||||
flags: ast::StringLiteralFlags::default(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn visit_annotated_slice(&self, slice: &mut Expr) {
|
||||
// we only want to walk the first tuple element if it exists,
|
||||
// anything else should not be transformed
|
||||
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice {
|
||||
if !elts.is_empty() {
|
||||
self.visit_expr(&mut elts[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SourceOrderVisitor<'a> for QuoteAnnotator<'a> {
|
||||
fn enter_node(&mut self, _node: ast::AnyNodeRef<'a>) -> TraversalSignal {
|
||||
if self.cannot_fix {
|
||||
TraversalSignal::Skip
|
||||
} else {
|
||||
TraversalSignal::Traverse
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_expr(&mut self, expr: &'a Expr) {
|
||||
let generator = Generator::from(self.stylist);
|
||||
|
||||
impl Transformer for QuoteAnnotator<'_> {
|
||||
fn visit_expr(&self, expr: &mut Expr) {
|
||||
match expr {
|
||||
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
|
||||
let value_expr = generator.expr(value);
|
||||
self.annotation.push_str(&value_expr);
|
||||
self.annotation.push('[');
|
||||
|
||||
if let Some(qualified_name) = self.semantic.resolve_qualified_name(value) {
|
||||
if self
|
||||
.semantic
|
||||
.match_typing_qualified_name(&qualified_name, "Literal")
|
||||
{
|
||||
self.state.push(QuoteAnnotatorState::Literal);
|
||||
// we don't want to modify anything inside `Literal`
|
||||
// so skip visiting this subscripts' slice
|
||||
} else if self
|
||||
.semantic
|
||||
.match_typing_qualified_name(&qualified_name, "Annotated")
|
||||
{
|
||||
self.state.push(QuoteAnnotatorState::AnnotatedFirst);
|
||||
self.visit_annotated_slice(slice);
|
||||
} else {
|
||||
self.state.push(QuoteAnnotatorState::Other);
|
||||
self.visit_expr(slice);
|
||||
}
|
||||
}
|
||||
|
||||
self.visit_expr(slice);
|
||||
self.state.pop();
|
||||
self.annotation.push(']');
|
||||
}
|
||||
Expr::Tuple(ast::ExprTuple { elts, .. }) => {
|
||||
let Some((first, remaining)) = elts.split_first() else {
|
||||
return;
|
||||
};
|
||||
self.visit_expr(first);
|
||||
if let Some(last) = self.state.last_mut() {
|
||||
if *last == QuoteAnnotatorState::AnnotatedFirst {
|
||||
*last = QuoteAnnotatorState::AnnotatedRest;
|
||||
}
|
||||
}
|
||||
for expr in remaining {
|
||||
self.annotation.push_str(", ");
|
||||
Expr::StringLiteral(literal) => {
|
||||
// try to parse the forward reference and replace the string
|
||||
// literal node with the parsed expression, if we fail to
|
||||
// parse the forward reference, we just keep treating this
|
||||
// like a regular string literal
|
||||
if let Ok(annotation) = parse_type_annotation(literal, self.locator.contents()) {
|
||||
*expr = annotation.expression().clone();
|
||||
// we need to visit the parsed expression too
|
||||
// since it may contain forward references itself
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
self.state.pop();
|
||||
}
|
||||
// For the following expressions, we just need to make sure to visit the nested
|
||||
// expressions using the quote annotator and not use the generator. This is so that any
|
||||
// subscript elements nested within them are identified and quoted correctly.
|
||||
Expr::List(ast::ExprList { elts, .. }) => {
|
||||
let mut first = true;
|
||||
self.annotation.push('[');
|
||||
for expr in elts {
|
||||
if first {
|
||||
first = false;
|
||||
} else {
|
||||
self.annotation.push_str(", ");
|
||||
}
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
self.annotation.push(']');
|
||||
}
|
||||
Expr::BinOp(ast::ExprBinOp {
|
||||
left, op, right, ..
|
||||
}) => {
|
||||
debug_assert_eq!(*op, ast::Operator::BitOr);
|
||||
self.visit_expr(left);
|
||||
self.annotation.push_str(" | ");
|
||||
self.visit_expr(right);
|
||||
}
|
||||
_ => {
|
||||
let source = match self.state.last().copied() {
|
||||
Some(QuoteAnnotatorState::Literal | QuoteAnnotatorState::AnnotatedRest) => {
|
||||
let mut source = generator.expr(expr);
|
||||
let opposite_quote = &self.stylist.quote().opposite().as_char().to_string();
|
||||
// If the quotes we are going to insert in this source already exists set the auto quote outcome
|
||||
// to failed. Because this means we are inserting quotes that are in the string and they collect.
|
||||
if source.contains(opposite_quote) || source.contains('\\') {
|
||||
self.cannot_fix = true;
|
||||
}
|
||||
source = source.replace(self.stylist.quote().as_char(), opposite_quote);
|
||||
source
|
||||
}
|
||||
None
|
||||
| Some(QuoteAnnotatorState::AnnotatedFirst | QuoteAnnotatorState::Other) => {
|
||||
let mut source = generator.expr(expr);
|
||||
source = source.replace(self.stylist.quote().as_char(), "");
|
||||
source = source.replace(self.stylist.quote().opposite().as_char(), "");
|
||||
source
|
||||
}
|
||||
};
|
||||
self.annotation.push_str(&source);
|
||||
walk_expr(self, expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use ruff_python_ast::Expr;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
|
|
@ -34,16 +34,14 @@ use crate::rules::flake8_type_checking::helpers::quote_type_expression;
|
|||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct RuntimeCastValue;
|
||||
|
||||
impl Violation for RuntimeCastValue {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
impl AlwaysFixableViolation for RuntimeCastValue {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"Add quotes to type expression in `typing.cast()`".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Add quotes".to_string())
|
||||
fn fix_title(&self) -> String {
|
||||
"Add quotes".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -54,16 +52,19 @@ pub(crate) fn runtime_cast_value(checker: &mut Checker, type_expr: &Expr) {
|
|||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(RuntimeCastValue, type_expr.range());
|
||||
let edit = quote_type_expression(type_expr, checker.semantic(), checker.stylist()).ok();
|
||||
if let Some(edit) = edit {
|
||||
if checker
|
||||
.comment_ranges()
|
||||
.has_comments(type_expr, checker.source())
|
||||
{
|
||||
diagnostic.set_fix(Fix::unsafe_edit(edit));
|
||||
} else {
|
||||
diagnostic.set_fix(Fix::safe_edit(edit));
|
||||
}
|
||||
let edit = quote_type_expression(
|
||||
type_expr,
|
||||
checker.semantic(),
|
||||
checker.stylist(),
|
||||
checker.locator(),
|
||||
);
|
||||
if checker
|
||||
.comment_ranges()
|
||||
.has_comments(type_expr, checker.source())
|
||||
{
|
||||
diagnostic.set_fix(Fix::unsafe_edit(edit));
|
||||
} else {
|
||||
diagnostic.set_fix(Fix::safe_edit(edit));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ pub(crate) fn runtime_import_in_type_checking_block(
|
|||
// Generate a diagnostic for every import, but share a fix across all imports within the same
|
||||
// statement (excluding those that are ignored).
|
||||
Action::Quote => {
|
||||
let fix = quote_imports(checker, node_id, &imports).ok();
|
||||
let fix = quote_imports(checker, node_id, &imports);
|
||||
|
||||
for ImportBinding {
|
||||
import,
|
||||
|
|
@ -232,9 +232,7 @@ pub(crate) fn runtime_import_in_type_checking_block(
|
|||
if let Some(range) = parent_range {
|
||||
diagnostic.set_parent(range.start());
|
||||
}
|
||||
if let Some(fix) = fix.as_ref() {
|
||||
diagnostic.set_fix(fix.clone());
|
||||
}
|
||||
diagnostic.set_fix(fix.clone());
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
|
@ -267,7 +265,7 @@ pub(crate) fn runtime_import_in_type_checking_block(
|
|||
}
|
||||
|
||||
/// Generate a [`Fix`] to quote runtime usages for imports in a type-checking block.
|
||||
fn quote_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) -> Result<Fix> {
|
||||
fn quote_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) -> Fix {
|
||||
let quote_reference_edits = filter_contained(
|
||||
imports
|
||||
.iter()
|
||||
|
|
@ -279,20 +277,21 @@ fn quote_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding])
|
|||
reference.expression_id()?,
|
||||
checker.semantic(),
|
||||
checker.stylist(),
|
||||
checker.locator(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
let mut rest = quote_reference_edits.into_iter();
|
||||
let head = rest.next().expect("Expected at least one reference");
|
||||
Ok(Fix::unsafe_edits(head, rest).isolate(Checker::isolation(
|
||||
Fix::unsafe_edits(head, rest).isolate(Checker::isolation(
|
||||
checker.semantic().parent_statement_id(node_id),
|
||||
)))
|
||||
))
|
||||
}
|
||||
|
||||
/// Generate a [`Fix`] to remove runtime imports from a type-checking block.
|
||||
|
|
|
|||
|
|
@ -153,14 +153,17 @@ pub(crate) fn unquoted_type_alias(checker: &Checker, binding: &Binding) -> Optio
|
|||
// Eventually we may try to be more clever and come up with the
|
||||
// minimal set of subexpressions that need to be quoted.
|
||||
let parent = expr.range().start();
|
||||
let edit = quote_type_expression(expr, checker.semantic(), checker.stylist());
|
||||
let edit = quote_type_expression(
|
||||
expr,
|
||||
checker.semantic(),
|
||||
checker.stylist(),
|
||||
checker.locator(),
|
||||
);
|
||||
let mut diagnostics = Vec::with_capacity(names.len());
|
||||
for name in names {
|
||||
let mut diagnostic = Diagnostic::new(UnquotedTypeAlias, name.range());
|
||||
diagnostic.set_parent(parent);
|
||||
if let Ok(edit) = edit.as_ref() {
|
||||
diagnostic.set_fix(Fix::unsafe_edit(edit.clone()));
|
||||
}
|
||||
diagnostic.set_fix(Fix::unsafe_edit(edit.clone()));
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
Some(diagnostics)
|
||||
|
|
|
|||
|
|
@ -509,13 +509,14 @@ fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) ->
|
|||
reference.expression_id()?,
|
||||
checker.semantic(),
|
||||
checker.stylist(),
|
||||
checker.locator(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
Ok(Fix::unsafe_edits(
|
||||
|
|
|
|||
|
|
@ -152,18 +152,38 @@ quote3.py:40:37: TC002 [*] Move third-party import `django.contrib.auth.models`
|
|||
44 47 |
|
||||
45 48 |
|
||||
|
||||
quote3.py:59:29: TC002 Move third-party import `third_party.Type` into a type-checking block
|
||||
quote3.py:59:29: TC002 [*] Move third-party import `third_party.Type` into a type-checking block
|
||||
|
|
||||
57 | def f():
|
||||
58 | from typing import Literal
|
||||
59 | from third_party import Type
|
||||
| ^^^^ TC002
|
||||
60 |
|
||||
61 | def test_string_contains_opposite_quote_do_not_fix(self, type1: Type[Literal["'"]], type2: Type[Literal["\'"]]):
|
||||
61 | def test_string_contains_opposite_quote(self, type1: Type[Literal["'"]], type2: Type[Literal["\'"]]):
|
||||
|
|
||||
= help: Move into type-checking block
|
||||
|
||||
quote3.py:67:29: TC002 Move third-party import `third_party.Type` into a type-checking block
|
||||
ℹ Unsafe fix
|
||||
1 |+from typing import TYPE_CHECKING
|
||||
2 |+
|
||||
3 |+if TYPE_CHECKING:
|
||||
4 |+ from third_party import Type
|
||||
1 5 | def f():
|
||||
2 6 | from typing import Literal, Union
|
||||
3 7 |
|
||||
--------------------------------------------------------------------------------
|
||||
56 60 |
|
||||
57 61 | def f():
|
||||
58 62 | from typing import Literal
|
||||
59 |- from third_party import Type
|
||||
60 63 |
|
||||
61 |- def test_string_contains_opposite_quote(self, type1: Type[Literal["'"]], type2: Type[Literal["\'"]]):
|
||||
64 |+ def test_string_contains_opposite_quote(self, type1: 'Type[Literal["\'"]]', type2: 'Type[Literal["\'"]]'):
|
||||
62 65 | pass
|
||||
63 66 |
|
||||
64 67 |
|
||||
|
||||
quote3.py:67:29: TC002 [*] Move third-party import `third_party.Type` into a type-checking block
|
||||
|
|
||||
65 | def f():
|
||||
66 | from typing import Literal
|
||||
|
|
@ -173,3 +193,21 @@ quote3.py:67:29: TC002 Move third-party import `third_party.Type` into a type-ch
|
|||
69 | def test_quote_contains_backslash(self, type1: Type[Literal["\n"]], type2: Type[Literal["\""]]):
|
||||
|
|
||||
= help: Move into type-checking block
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from typing import TYPE_CHECKING
|
||||
2 |+
|
||||
3 |+if TYPE_CHECKING:
|
||||
4 |+ from third_party import Type
|
||||
1 5 | def f():
|
||||
2 6 | from typing import Literal, Union
|
||||
3 7 |
|
||||
--------------------------------------------------------------------------------
|
||||
64 68 |
|
||||
65 69 | def f():
|
||||
66 70 | from typing import Literal
|
||||
67 |- from third_party import Type
|
||||
68 71 |
|
||||
69 |- def test_quote_contains_backslash(self, type1: Type[Literal["\n"]], type2: Type[Literal["\""]]):
|
||||
72 |+ def test_quote_contains_backslash(self, type1: 'Type[Literal["\\n"]]', type2: 'Type[Literal[\'"\']]'):
|
||||
70 73 | pass
|
||||
|
|
|
|||
|
|
@ -136,3 +136,58 @@ TC006.py:59:9: TC006 [*] Add quotes to type expression in `typing.cast()`
|
|||
59 |+ "int | None",
|
||||
61 60 | 3.0
|
||||
62 61 | )
|
||||
63 62 |
|
||||
|
||||
TC006.py:68:17: TC006 [*] Add quotes to type expression in `typing.cast()`
|
||||
|
|
||||
66 | # Regression test for #14554
|
||||
67 | import typing
|
||||
68 | typing.cast(M-())
|
||||
| ^^^^ TC006
|
||||
|
|
||||
= help: Add quotes
|
||||
|
||||
ℹ Safe fix
|
||||
65 65 | def f():
|
||||
66 66 | # Regression test for #14554
|
||||
67 67 | import typing
|
||||
68 |- typing.cast(M-())
|
||||
68 |+ typing.cast("M - ()")
|
||||
69 69 |
|
||||
70 70 |
|
||||
71 71 | def f():
|
||||
|
||||
TC006.py:75:10: TC006 [*] Add quotes to type expression in `typing.cast()`
|
||||
|
|
||||
73 | from typing import cast, Literal
|
||||
74 |
|
||||
75 | cast(Literal["A"], 'A')
|
||||
| ^^^^^^^^^^^^ TC006
|
||||
|
|
||||
= help: Add quotes
|
||||
|
||||
ℹ Safe fix
|
||||
72 72 | # Simple case with Literal that should lead to nested quotes
|
||||
73 73 | from typing import cast, Literal
|
||||
74 74 |
|
||||
75 |- cast(Literal["A"], 'A')
|
||||
75 |+ cast("Literal['A']", 'A')
|
||||
76 76 |
|
||||
77 77 |
|
||||
78 78 | def f():
|
||||
|
||||
TC006.py:82:10: TC006 [*] Add quotes to type expression in `typing.cast()`
|
||||
|
|
||||
80 | from typing import cast, Annotated, Literal
|
||||
81 |
|
||||
82 | cast(list[Annotated["list['Literal[\"A\"]']", "Foo"]], ['A'])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TC006
|
||||
|
|
||||
= help: Add quotes
|
||||
|
||||
ℹ Safe fix
|
||||
79 79 | # Really complex case with nested forward references
|
||||
80 80 | from typing import cast, Annotated, Literal
|
||||
81 81 |
|
||||
82 |- cast(list[Annotated["list['Literal[\"A\"]']", "Foo"]], ['A'])
|
||||
82 |+ cast("list[Annotated[list[Literal['A']], 'Foo']]", ['A'])
|
||||
|
|
|
|||
Loading…
Reference in New Issue