mirror of
https://github.com/astral-sh/ruff.git
synced 2026-10-28 10:45:37 +08:00
[ty] Explain invalid aliases at their definitions (#29654)
Builds on #29200. When an invalid implicit alias is used as a type, the error now explains why its original right-hand side is invalid and points to that expression. For example: ```py Called = type(1) # snapshot: invalid-type-form def inspect(called: Called): reveal_type(called) # revealed: Unknown ``` ```text error[invalid-type-form]: Variable is not allowed in parameter annotations --> src/mdtest_snippet.py:14:21 | 14 | def inspect(called: Called): | ^^^^^^ info: Function calls are not allowed in type alias values --> src/mdtest_snippet.py:11:10 | 11 | Called = type(1) | ^^^^^^^ info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions ``` No other type checker provides this kind of detailed origin explanation when a value is not usable as a type alias. Follow imports and forwarding assignments to the original definition. Collect these explanations independently of diagnostics in the defining file, so unchecked dependencies and local suppressions do not hide them. Ordinary assignments remain valid at runtime; the error is reported only where the value is used as a type.
This commit is contained in:
1 parent
c08d12bfc9
commit
ea1865842b
15 files changed
+309
-103
No files matched your search
@@ -36,7 +36,7 @@ reveal_type(Strings) # revealed: GenericAlias
|
||||
However, using such a `GenericAlias` instance in a type expression is currently not supported:
|
||||
|
||||
```py
|
||||
# error: [invalid-type-form] "Variable of type `GenericAlias` is not allowed in a parameter annotation"
|
||||
# error: [invalid-type-form] "Variable `Strings` cannot be used as a type"
|
||||
def _(strings: Strings) -> None:
|
||||
reveal_type(strings) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -139,17 +139,17 @@ class Mat:
|
||||
return 42
|
||||
|
||||
def invalid_binary_operators(
|
||||
a: "1" + "2", # error: [invalid-type-form] "Invalid binary operator `+` in type annotation"
|
||||
b: 3 - 5.0, # error: [invalid-type-form] "Invalid binary operator `-` in type annotation"
|
||||
c: 4 * -2, # error: [invalid-type-form] "Invalid binary operator `*` in type annotation"
|
||||
d: Mat(4) @ Mat(2), # error: [invalid-type-form] "Invalid binary operator `@` in type annotation"
|
||||
e: 10 / 2, # error: [invalid-type-form] "Invalid binary operator `/` in type annotation"
|
||||
f: 10 % 3, # error: [invalid-type-form] "Invalid binary operator `%` in type annotation"
|
||||
g: 2**-0.5, # error: [invalid-type-form] "Invalid binary operator `**` in type annotation"
|
||||
h: 10 // 3, # error: [invalid-type-form] "Invalid binary operator `//` in type annotation"
|
||||
i: 1 << 2, # error: [invalid-type-form] "Invalid binary operator `<<` in type annotation"
|
||||
j: 4 >> 42, # error: [invalid-type-form] "Invalid binary operator `>>` in type annotation"
|
||||
k: 5 ^ 3, # error: [invalid-type-form] "Invalid binary operator `^` in type annotation"
|
||||
a: "1" + "2", # error: [invalid-type-form] "Invalid binary operator `+` in a parameter annotation"
|
||||
b: 3 - 5.0, # error: [invalid-type-form] "Invalid binary operator `-` in a parameter annotation"
|
||||
c: 4 * -2, # error: [invalid-type-form] "Invalid binary operator `*` in a parameter annotation"
|
||||
d: Mat(4) @ Mat(2), # error: [invalid-type-form] "Invalid binary operator `@` in a parameter annotation"
|
||||
e: 10 / 2, # error: [invalid-type-form] "Invalid binary operator `/` in a parameter annotation"
|
||||
f: 10 % 3, # error: [invalid-type-form] "Invalid binary operator `%` in a parameter annotation"
|
||||
g: 2**-0.5, # error: [invalid-type-form] "Invalid binary operator `**` in a parameter annotation"
|
||||
h: 10 // 3, # error: [invalid-type-form] "Invalid binary operator `//` in a parameter annotation"
|
||||
i: 1 << 2, # error: [invalid-type-form] "Invalid binary operator `<<` in a parameter annotation"
|
||||
j: 4 >> 42, # error: [invalid-type-form] "Invalid binary operator `>>` in a parameter annotation"
|
||||
k: 5 ^ 3, # error: [invalid-type-form] "Invalid binary operator `^` in a parameter annotation"
|
||||
# error: [invalid-type-form] "Int literals are not allowed in this context in a parameter annotation"
|
||||
# error: [invalid-type-form] "Int literals are not allowed in this context in a parameter annotation"
|
||||
l: 5 & 3,
|
||||
@@ -1274,7 +1274,7 @@ class C:
|
||||
```
|
||||
|
||||
```snapshot
|
||||
error[invalid-type-form]: List literals are not allowed in this context in a type alias value
|
||||
error[invalid-type-form]: List literals are not allowed in this context in a type alias definition
|
||||
--> src/mdtest_snippet.py:6:21
|
||||
|
|
||||
6 | type Alias[T] = [int] # snapshot: invalid-type-form
|
||||
|
||||
@@ -410,8 +410,8 @@ annotations and preserve the class binding, so constructor calls can still be ch
|
||||
```pyi
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
# error: [invalid-type-form] "Variable of type `Example` is not allowed in a parameter annotation"
|
||||
# error: [invalid-type-form] "Variable of type `Example` is not allowed in a return type annotation"
|
||||
# error: [invalid-type-form] "Variable `annotation` cannot be used as a type"
|
||||
# error: [invalid-type-form] "Variable `annotation` cannot be used as a type"
|
||||
def identity(value: annotation) -> annotation: ...
|
||||
|
||||
@identity
|
||||
@@ -467,8 +467,8 @@ on the decorated class.
|
||||
from dataclasses import dataclass
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
# error: [invalid-type-form] "Variable of type `Example` is not allowed in a parameter annotation"
|
||||
# error: [invalid-type-form] "Variable of type `Example` is not allowed in a return type annotation"
|
||||
# error: [invalid-type-form] "Variable `annotation` cannot be used as a type"
|
||||
# error: [invalid-type-form] "Variable `annotation` cannot be used as a type"
|
||||
def identity(value: annotation) -> annotation: ...
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -449,7 +449,7 @@ def _():
|
||||
type X[T: (yield 1)] = int
|
||||
|
||||
def _():
|
||||
# error: [invalid-type-form] "`yield` expressions are not allowed in type alias values"
|
||||
# error: [invalid-type-form] "`yield` expressions are not allowed in type alias definitions"
|
||||
# error: [invalid-syntax] "yield expression cannot be used within a type alias"
|
||||
type Y = (yield 1)
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ reveal_type(SpecializedList[str]) # revealed: Unknown
|
||||
def _(l: List[int][int]):
|
||||
reveal_type(l) # revealed: Unknown
|
||||
|
||||
# error: [invalid-type-form] "Only simple names and dotted names can be subscripted in type alias values"
|
||||
# error: [invalid-type-form] "Only simple names and dotted names can be subscripted in type alias definitions"
|
||||
type DoubleSpecialization[T] = list[T][T]
|
||||
|
||||
def _(d: DoubleSpecialization[int]):
|
||||
|
||||
@@ -2337,16 +2337,23 @@ def inspect(
|
||||
```
|
||||
|
||||
```snapshot
|
||||
error[invalid-type-form]: Variable is not allowed in parameter annotations
|
||||
error[invalid-type-form]: Variable `Number` cannot be used as a type
|
||||
--> src/mdtest_snippet.py:6:17
|
||||
|
|
||||
6 | expression: Number, # snapshot: invalid-type-form
|
||||
| ^^^^^^
|
||||
info: `Number` is not a valid type alias
|
||||
info: Invalid binary operator `+` in a type alias definition
|
||||
--> src/mdtest_snippet.py:1:10
|
||||
|
|
||||
1 | Number = 1 + 2
|
||||
| ^^^^^
|
||||
info: See the following page for a reference on valid type expressions:
|
||||
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
|
||||
```
|
||||
|
||||
An expression that evaluates to a class does not automatically define a type alias:
|
||||
An expression that evaluates to a class does not automatically define a type alias. The diagnostic
|
||||
explains why its original definition is invalid as a type expression:
|
||||
|
||||
```py
|
||||
Called = type(1)
|
||||
@@ -2357,11 +2364,17 @@ def inspect(called: Called):
|
||||
```
|
||||
|
||||
```snapshot
|
||||
error[invalid-type-form]: Variable is not allowed in parameter annotations
|
||||
error[invalid-type-form]: Variable `Called` cannot be used as a type
|
||||
--> src/mdtest_snippet.py:18:21
|
||||
|
|
||||
18 | def inspect(called: Called):
|
||||
| ^^^^^^
|
||||
info: `Called` is not a valid type alias
|
||||
info: Function calls are not allowed in type alias definitions
|
||||
--> src/mdtest_snippet.py:15:10
|
||||
|
|
||||
15 | Called = type(1)
|
||||
| ^^^^^^^
|
||||
info: See the following page for a reference on valid type expressions:
|
||||
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
|
||||
```
|
||||
@@ -2469,11 +2482,45 @@ def inspect_forwarded(forwarded: ForwardedClass):
|
||||
```
|
||||
|
||||
```snapshot
|
||||
error[invalid-type-form]: Variable is not allowed in parameter annotations
|
||||
error[invalid-type-form]: Variable `ForwardedClass` cannot be used as a type
|
||||
--> src/main.py:4:34
|
||||
|
|
||||
4 | def inspect_forwarded(forwarded: ForwardedClass):
|
||||
| ^^^^^^^^^^^^^^
|
||||
info: `ForwardedClass` is not a valid type alias
|
||||
info: Function calls are not allowed in type alias definitions
|
||||
--> dependencies/values.py:3:17
|
||||
|
|
||||
3 | ComputedClass = (lambda: int)()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
info: See the following page for a reference on valid type expressions:
|
||||
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
|
||||
```
|
||||
|
||||
Quoted attribute references name the attribute used in the annotation, while the explanation points
|
||||
to its original definition:
|
||||
|
||||
`quoted.py`:
|
||||
|
||||
```py
|
||||
import values
|
||||
|
||||
# snapshot: invalid-type-form
|
||||
def inspect_quoted(attribute: "values.Number"): ...
|
||||
```
|
||||
|
||||
```snapshot
|
||||
error[invalid-type-form]: Variable `values.Number` cannot be used as a type
|
||||
--> src/quoted.py:4:32
|
||||
|
|
||||
4 | def inspect_quoted(attribute: "values.Number"): ...
|
||||
| ^^^^^^^^^^^^^
|
||||
info: `values.Number` is not a valid type alias
|
||||
info: Invalid binary operator `+` in a type alias definition
|
||||
--> dependencies/values.py:1:10
|
||||
|
|
||||
1 | Number = 1 + 2
|
||||
| ^^^^^
|
||||
info: See the following page for a reference on valid type expressions:
|
||||
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
|
||||
```
|
||||
@@ -2505,6 +2552,39 @@ def inspect_classes(
|
||||
reveal_type(local_attribute) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Suppressions on alias definitions
|
||||
|
||||
The assignment below is valid runtime code; the error belongs to the use of `Number` as a type.
|
||||
|
||||
We explain the error by inferring the assignment's right-hand side as a type expression at its
|
||||
original source location. The resulting diagnostics are attached as notes on the error at the use,
|
||||
not reported as separate errors on the assignment. This inference must bypass the assignment's
|
||||
suppression: the test checks that the ignore remains unused and that the explanation is still shown.
|
||||
|
||||
```py
|
||||
# error: [unused-ignore-comment]
|
||||
Number = 1 + 2 # ty: ignore[invalid-type-form]
|
||||
|
||||
# snapshot: invalid-type-form
|
||||
def inspect(value: Number): ...
|
||||
```
|
||||
|
||||
```snapshot
|
||||
error[invalid-type-form]: Variable `Number` cannot be used as a type
|
||||
--> src/mdtest_snippet.py:5:20
|
||||
|
|
||||
5 | def inspect(value: Number): ...
|
||||
| ^^^^^^
|
||||
info: `Number` is not a valid type alias
|
||||
info: Invalid binary operator `+` in a type alias definition
|
||||
--> src/mdtest_snippet.py:2:10
|
||||
|
|
||||
2 | Number = 1 + 2 # ty: ignore[invalid-type-form]
|
||||
| ^^^^^
|
||||
info: See the following page for a reference on valid type expressions:
|
||||
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
|
||||
```
|
||||
|
||||
## Disabled invalid implicit alias diagnostics
|
||||
|
||||
Disabling `invalid-type-form` does not make invalid aliases valid:
|
||||
|
||||
@@ -1077,7 +1077,7 @@ differently_bad: TypeAlias = Unpack[tuple[int, ...]] # snapshot: invalid-type-f
|
||||
```
|
||||
|
||||
```snapshot
|
||||
error[invalid-type-form]: `Unpack` is not allowed in type alias values
|
||||
error[invalid-type-form]: `Unpack` is not allowed in type alias definitions
|
||||
--> src/mdtest_snippet.py:14:30
|
||||
|
|
||||
14 | differently_bad: TypeAlias = Unpack[tuple[int, ...]] # snapshot: invalid-type-form
|
||||
|
||||
@@ -135,23 +135,23 @@ appear at the top level of a PEP 695 alias definition:
|
||||
from typing_extensions import ClassVar, Final, Required, NotRequired, ReadOnly
|
||||
from dataclasses import InitVar
|
||||
|
||||
# error: [invalid-type-form] "Type qualifier `typing.ClassVar` is not allowed in type alias values"
|
||||
# error: [invalid-type-form] "Type qualifier `typing.ClassVar` is not allowed in type alias definitions"
|
||||
type Bad1 = ClassVar[str]
|
||||
# error: [invalid-type-form] "Type qualifier `typing.ClassVar` is not allowed in type alias values"
|
||||
# error: [invalid-type-form] "Type qualifier `typing.ClassVar` is not allowed in type alias definitions"
|
||||
type Bad2 = ClassVar
|
||||
# error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type alias values"
|
||||
# error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type alias definitions"
|
||||
type Bad3 = Final[int]
|
||||
# error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type alias values"
|
||||
# error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type alias definitions"
|
||||
type Bad4 = Final
|
||||
# error: [invalid-type-form] "Type qualifier `typing.Required` is not allowed in type alias values"
|
||||
# error: [invalid-type-form] "Type qualifier `typing.Required` is not allowed in type alias definitions"
|
||||
type Bad5 = Required[int]
|
||||
# error: [invalid-type-form] "Type qualifier `typing.NotRequired` is not allowed in type alias values"
|
||||
# error: [invalid-type-form] "Type qualifier `typing.NotRequired` is not allowed in type alias definitions"
|
||||
type Bad6 = NotRequired[int]
|
||||
# error: [invalid-type-form] "Type qualifier `typing.ReadOnly` is not allowed in type alias values"
|
||||
# error: [invalid-type-form] "Type qualifier `typing.ReadOnly` is not allowed in type alias definitions"
|
||||
type Bad7 = ReadOnly[int]
|
||||
# error: [invalid-type-form] "Type qualifier `dataclasses.InitVar` is not allowed in type alias values"
|
||||
# error: [invalid-type-form] "Type qualifier `dataclasses.InitVar` is not allowed in type alias definitions"
|
||||
type Bad8 = InitVar[int]
|
||||
# error: [invalid-type-form] "Type qualifier `dataclasses.InitVar` is not allowed in type alias values"
|
||||
# error: [invalid-type-form] "Type qualifier `dataclasses.InitVar` is not allowed in type alias definitions"
|
||||
type Bad9 = InitVar
|
||||
```
|
||||
|
||||
|
||||
+1
-1
@@ -216,7 +216,7 @@ info: - or as part of an argument list when specializing a generic class
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type alias value
|
||||
error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a type alias definition
|
||||
--> src/mdtest_snippet.py:33:19
|
||||
|
|
||||
33 | type Alias[**P] = P
|
||||
|
||||
@@ -78,7 +78,7 @@ pub(crate) use crate::types::class_base::ClassBase;
|
||||
use crate::types::constraints::ConstraintSetBuilder;
|
||||
use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder};
|
||||
use crate::types::diagnostic::{
|
||||
AttributeAccessMethod, INVALID_AWAIT, INVALID_TYPE_FORM, report_bad_attribute_access_call,
|
||||
AttributeAccessMethod, INVALID_AWAIT, report_bad_attribute_access_call,
|
||||
report_bad_dunder_get_call, report_bad_import_call,
|
||||
};
|
||||
pub use crate::types::display::{DisplaySettings, TypeDetail, TypeDisplayDetails};
|
||||
@@ -11792,30 +11792,6 @@ pub struct InvalidTypeExpressionError<'db> {
|
||||
invalid_expressions: smallvec::SmallVec<[InvalidTypeExpression<'db>; 1]>,
|
||||
}
|
||||
|
||||
impl<'db> InvalidTypeExpressionError<'db> {
|
||||
fn into_fallback_type(
|
||||
self,
|
||||
context: &InferContext,
|
||||
node: &impl Ranged,
|
||||
flags: InferenceFlags,
|
||||
) -> Type<'db> {
|
||||
let db = context.db();
|
||||
let InvalidTypeExpressionError {
|
||||
fallback_type,
|
||||
invalid_expressions,
|
||||
} = self;
|
||||
let env = context.program_environment();
|
||||
for error in invalid_expressions {
|
||||
let Some(builder) = context.report_lint(&INVALID_TYPE_FORM, node) else {
|
||||
continue;
|
||||
};
|
||||
let diagnostic = builder.into_diagnostic(error.reason(db, env, flags));
|
||||
error.add_subdiagnostics(db, env, diagnostic, node);
|
||||
}
|
||||
fallback_type
|
||||
}
|
||||
}
|
||||
|
||||
/// Enumeration of various types that are invalid in type-expression contexts
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize, salsa::SalsaValue)]
|
||||
enum InvalidTypeExpression<'db> {
|
||||
|
||||
@@ -126,6 +126,14 @@ enum ProgramSource {
|
||||
Scope(Id),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
enum DiagnosticMode {
|
||||
Normal,
|
||||
Suppressed,
|
||||
/// Explain an invalid type expression without reporting errors in its defining file.
|
||||
TypeExpressionExplanation,
|
||||
}
|
||||
|
||||
/// Context for inferring the types of a single file.
|
||||
///
|
||||
/// One context exists for at least for every inferred region but it's
|
||||
@@ -147,7 +155,7 @@ pub(crate) struct InferContext<'db, 'ast> {
|
||||
module: &'ast ParsedModuleRef,
|
||||
diagnostics: std::cell::RefCell<TypeCheckDiagnostics>,
|
||||
has_invalid_type_form: Cell<bool>,
|
||||
diagnostics_suppressed: bool,
|
||||
diagnostic_mode: DiagnosticMode,
|
||||
/// This field tracks various flags that control how type inference should behave in the current context.
|
||||
pub(crate) inference_flags: InferenceFlags,
|
||||
bomb: DebugDropBomb,
|
||||
@@ -175,7 +183,7 @@ impl<'db, 'ast> InferContext<'db, 'ast> {
|
||||
program_file,
|
||||
diagnostics: std::cell::RefCell::new(TypeCheckDiagnostics::default()),
|
||||
has_invalid_type_form: Cell::new(false),
|
||||
diagnostics_suppressed: false,
|
||||
diagnostic_mode: DiagnosticMode::Normal,
|
||||
inference_flags: InferenceFlags::empty(),
|
||||
bomb: DebugDropBomb::new(
|
||||
"`InferContext` needs to be explicitly consumed by calling `::finish` to prevent accidental loss of diagnostics.",
|
||||
@@ -240,12 +248,23 @@ impl<'db, 'ast> InferContext<'db, 'ast> {
|
||||
|
||||
pub(crate) fn extend(&mut self, other: &InferContextResult) {
|
||||
*self.has_invalid_type_form.get_mut() |= other.has_invalid_type_form;
|
||||
self.diagnostics.get_mut().extend(&other.diagnostics);
|
||||
let diagnostics = self.diagnostics.get_mut();
|
||||
if self.diagnostic_mode == DiagnosticMode::TypeExpressionExplanation {
|
||||
let invalid_type_form = DiagnosticId::Lint(INVALID_TYPE_FORM.name());
|
||||
diagnostics.extend_diagnostics(
|
||||
(&other.diagnostics)
|
||||
.into_iter()
|
||||
.filter(|diagnostic| diagnostic.id() == invalid_type_form)
|
||||
.cloned(),
|
||||
);
|
||||
} else {
|
||||
diagnostics.extend(&other.diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether diagnostics inferred separately for a referenced alias should be collected.
|
||||
pub(super) fn should_collect_diagnostics(&self) -> bool {
|
||||
!self.diagnostics_suppressed
|
||||
self.diagnostic_mode == DiagnosticMode::Normal
|
||||
&& self.db().should_check_file(self.file())
|
||||
&& !self.is_in_no_type_check()
|
||||
}
|
||||
@@ -261,8 +280,21 @@ impl<'db, 'ast> InferContext<'db, 'ast> {
|
||||
|
||||
/// Prevents diagnostic construction for this inference context.
|
||||
pub(super) fn suppress_diagnostics(&mut self) {
|
||||
debug_assert!(!self.diagnostics_suppressed);
|
||||
self.diagnostics_suppressed = true;
|
||||
debug_assert_ne!(self.diagnostic_mode, DiagnosticMode::Suppressed);
|
||||
self.diagnostic_mode = DiagnosticMode::Suppressed;
|
||||
}
|
||||
|
||||
/// Collect type-expression errors for use as explanations at a reference site.
|
||||
///
|
||||
/// These errors describe why a value cannot be used as a type, independently of whether
|
||||
/// diagnostics are enabled or suppressed in the file defining that value.
|
||||
pub(super) fn collect_type_expression_diagnostics(&mut self) {
|
||||
debug_assert_eq!(self.diagnostic_mode, DiagnosticMode::Normal);
|
||||
self.diagnostic_mode = DiagnosticMode::TypeExpressionExplanation;
|
||||
}
|
||||
|
||||
pub(super) fn is_type_expression_explanation(&self) -> bool {
|
||||
self.diagnostic_mode == DiagnosticMode::TypeExpressionExplanation
|
||||
}
|
||||
|
||||
pub(super) fn is_lint_enabled(&self, lint: &'static LintMetadata) -> bool {
|
||||
@@ -410,7 +442,7 @@ impl fmt::Debug for InferContext<'_, '_> {
|
||||
.field("program_file", &self.program_file)
|
||||
.field("diagnostics", &self.diagnostics)
|
||||
.field("has_invalid_type_form", &self.has_invalid_type_form)
|
||||
.field("diagnostics_suppressed", &self.diagnostics_suppressed)
|
||||
.field("diagnostic_mode", &self.diagnostic_mode)
|
||||
.field("inference_flags", &self.inference_flags)
|
||||
.finish()
|
||||
}
|
||||
@@ -559,7 +591,7 @@ impl Drop for LintDiagnosticGuard<'_, '_> {
|
||||
diag.clear_concise_message();
|
||||
}
|
||||
|
||||
if self.ctx.db().verbose() {
|
||||
if !self.ctx.is_type_expression_explanation() && self.ctx.db().verbose() {
|
||||
let rule = diag.id();
|
||||
|
||||
diag.info(match self.source {
|
||||
@@ -628,8 +660,13 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> {
|
||||
ctx: &'ctx InferContext<'db, 'ctx>,
|
||||
lint: LintId,
|
||||
) -> Option<(Severity, LintSource)> {
|
||||
if ctx.diagnostics_suppressed {
|
||||
return None;
|
||||
match ctx.diagnostic_mode {
|
||||
DiagnosticMode::Normal => {}
|
||||
DiagnosticMode::Suppressed => return None,
|
||||
DiagnosticMode::TypeExpressionExplanation => {
|
||||
return (lint == LintId::of(&INVALID_TYPE_FORM))
|
||||
.then_some((Severity::Error, LintSource::Default));
|
||||
}
|
||||
}
|
||||
|
||||
// The comment below was copied from the original
|
||||
@@ -665,17 +702,19 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> {
|
||||
) -> Option<LintDiagnosticGuardBuilder<'db, 'ctx>> {
|
||||
let (severity, source) = Self::severity_and_source(ctx, lint_id)?;
|
||||
|
||||
let suppressions = suppressions(ctx.db(), ctx.python_file());
|
||||
if let Some(suppression) = suppressions.find_suppression(range, lint_id) {
|
||||
ctx.diagnostics.borrow_mut().mark_used(suppression.id());
|
||||
return None;
|
||||
}
|
||||
if !ctx.is_type_expression_explanation() {
|
||||
let suppressions = suppressions(ctx.db(), ctx.python_file());
|
||||
if let Some(suppression) = suppressions.find_suppression(range, lint_id) {
|
||||
ctx.diagnostics.borrow_mut().mark_used(suppression.id());
|
||||
return None;
|
||||
}
|
||||
|
||||
// Suppress diagnostics in unreachable code. This checks both whether
|
||||
// the scope itself is unreachable and whether the specific statement or
|
||||
// expression containing this diagnostic is unreachable.
|
||||
if !ctx.is_range_reachable(range) {
|
||||
return None;
|
||||
// Suppress diagnostics in unreachable code. This checks both whether
|
||||
// the scope itself is unreachable and whether the specific statement or
|
||||
// expression containing this diagnostic is unreachable.
|
||||
if !ctx.is_range_reachable(range) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(LintDiagnosticGuardBuilder {
|
||||
@@ -754,7 +793,7 @@ impl<'db, 'ctx> DiagnosticGuardBuilder<'db, 'ctx> {
|
||||
id: DiagnosticId,
|
||||
severity: Severity,
|
||||
) -> Option<DiagnosticGuardBuilder<'db, 'ctx>> {
|
||||
if ctx.diagnostics_suppressed {
|
||||
if ctx.diagnostic_mode != DiagnosticMode::Normal {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
@@ -157,6 +157,30 @@ pub(super) struct LegacyTypeAliasInference<'db> {
|
||||
pub(super) legacy_aliases: Box<[Definition<'db>]>,
|
||||
}
|
||||
|
||||
/// Explain why an ordinary assignment's right-hand side is not a valid type expression.
|
||||
///
|
||||
/// These diagnostics are attached to errors at references to the value. They are inferred
|
||||
/// separately so unchecked dependencies and local suppressions do not hide the explanation.
|
||||
#[salsa::tracked(
|
||||
returns(ref),
|
||||
cycle_initial=|_, _, _| TypeCheckDiagnostics::default(),
|
||||
heap_size=ruff_memory_usage::heap_size
|
||||
)]
|
||||
pub(super) fn non_alias_type_expression_diagnostics<'db>(
|
||||
db: &'db dyn Db,
|
||||
definition: Definition<'db>,
|
||||
) -> TypeCheckDiagnostics {
|
||||
let program_file = definition.program_file(db);
|
||||
let python_file = program_file.python_file(db);
|
||||
let module = parsed_module(db, python_file).load(db);
|
||||
let Some(value) = definition.kind(db).value(&module) else {
|
||||
return TypeCheckDiagnostics::default();
|
||||
};
|
||||
let env = ProgramEnvironment::from_file(program_file);
|
||||
TypeInferenceBuilder::for_definition(db, &env, definition, &module)
|
||||
.finish_non_alias_type_expression_diagnostics(definition, value)
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
/// Metadata for expressions inferred as type expressions.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
@@ -2485,7 +2509,7 @@ impl InferenceFlags {
|
||||
} else if self.contains(InferenceFlags::IN_PARAMETER_ANNOTATION) {
|
||||
"parameter annotation"
|
||||
} else if self.contains(InferenceFlags::IN_TYPE_ALIAS) {
|
||||
"type alias value"
|
||||
"type alias definition"
|
||||
} else {
|
||||
"type expression"
|
||||
}
|
||||
|
||||
@@ -12655,6 +12655,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
builder.deferred_state = deferred_state;
|
||||
builder.typevar_binding_context = typevar_binding_context;
|
||||
builder.context.inference_flags = self.inference_flags();
|
||||
if self.context.is_type_expression_explanation() {
|
||||
builder.context.collect_type_expression_diagnostics();
|
||||
}
|
||||
builder.expression_cache.clone_from(expression_cache);
|
||||
builder.reachability_cache.clone_from(reachability_cache);
|
||||
builder
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use itertools::Either;
|
||||
use ruff_db::diagnostic::Annotation;
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_diagnostics::{Edit, Fix};
|
||||
use ruff_python_ast::helpers::is_dotted_name;
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::name::{Name, UnqualifiedName};
|
||||
use ruff_python_ast::token::parenthesized_range;
|
||||
use ruff_python_ast::{self as ast, PythonVersion};
|
||||
use ruff_source_file::LineRanges;
|
||||
@@ -23,6 +23,7 @@ use crate::types::infer::legacy_type_alias::non_alias_assignment;
|
||||
use crate::types::infer::{
|
||||
CyclicTypeAliasError, InferenceFlags, LegacyTypeAliasInference, TypeExpressionFlags,
|
||||
infer_legacy_type_alias, legacy_type_alias_definition, legacy_type_alias_parameters,
|
||||
non_alias_type_expression_diagnostics,
|
||||
};
|
||||
use crate::types::signatures::{ConcatenateTail, Signature};
|
||||
use crate::types::special_form::{AliasSpec, LegacyStdlibAlias};
|
||||
@@ -34,9 +35,9 @@ use ty_python_core::scope::ScopeKind;
|
||||
use crate::types::{
|
||||
BindingContext, CallableType, DynamicType, GenericContext, IntersectionBuilder,
|
||||
IntersectionType, InvalidTypeExpression, KnownClass, KnownInstanceType, LintDiagnosticGuard,
|
||||
Parameter, Parameters, SpecialFormType, SubclassOfType, Type, TypeContext, TypeFormType,
|
||||
TypeGuardType, TypeIsType, TypeMapping, TypeVarKind, UnionBuilder, UnionType, any_over_type,
|
||||
binding_type, todo_type,
|
||||
Parameter, Parameters, SpecialFormType, SubclassOfType, Type, TypeCheckDiagnostics,
|
||||
TypeContext, TypeFormType, TypeGuardType, TypeIsType, TypeMapping, TypeVarKind, UnionBuilder,
|
||||
UnionType, any_over_type, binding_type, todo_type,
|
||||
};
|
||||
use crate::{FxOrderSet, SemanticModel, add_inferred_python_version_hint_to_diagnostic};
|
||||
|
||||
@@ -130,6 +131,24 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
reference
|
||||
}
|
||||
|
||||
/// Infer an ordinary assignment's right-hand side as a type expression to explain
|
||||
/// why its value cannot be used as a type.
|
||||
///
|
||||
/// For example, `Number = type(1)` is valid at runtime, but using `Number` as a type
|
||||
/// is invalid because the right-hand side is a call. The caller attaches the returned
|
||||
/// `invalid-type-form` diagnostics as subdiagnostics at that reference. Collect them
|
||||
/// even when the defining file is unchecked or suppresses diagnostics.
|
||||
pub(in crate::types::infer) fn finish_non_alias_type_expression_diagnostics(
|
||||
mut self,
|
||||
definition: Definition<'db>,
|
||||
value: &ast::Expr,
|
||||
) -> TypeCheckDiagnostics {
|
||||
self.context.collect_type_expression_diagnostics();
|
||||
self.finish_legacy_type_alias(definition, value)
|
||||
.context
|
||||
.diagnostics
|
||||
}
|
||||
|
||||
pub(in crate::types::infer) fn finish_legacy_type_alias(
|
||||
mut self,
|
||||
definition: Definition<'db>,
|
||||
@@ -292,18 +311,35 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
return Type::unknown();
|
||||
}
|
||||
report_missing_type_arguments(&self.context, ty, annotation);
|
||||
let result_ty =
|
||||
result.unwrap_or_else(|error| {
|
||||
if error.invalid_expressions.iter().any(|invalid| {
|
||||
matches!(invalid, InvalidTypeExpression::InvalidBareTypeVarTuple(_))
|
||||
}) {
|
||||
let result_ty = result.unwrap_or_else(|error| {
|
||||
let causes = self.find_non_alias_diagnostics(ty, definition);
|
||||
for invalid in error.invalid_expressions {
|
||||
if matches!(invalid, InvalidTypeExpression::InvalidBareTypeVarTuple(_)) {
|
||||
self.store_type_expression_flags(
|
||||
annotation,
|
||||
TypeExpressionFlags::INVALID_BARE_TYPE_VAR_TUPLE,
|
||||
);
|
||||
}
|
||||
error.into_fallback_type(&self.context, annotation, self.inference_flags())
|
||||
});
|
||||
let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, annotation) else {
|
||||
continue;
|
||||
};
|
||||
let reason = invalid.reason(db, env, self.inference_flags());
|
||||
let diagnostic = if let Some(causes) = causes {
|
||||
let mut diagnostic = builder
|
||||
.with_message_override(
|
||||
Self::invalid_variable_type_expression_message(annotation),
|
||||
&Self::invalid_type_alias_message(annotation),
|
||||
)
|
||||
.into_diagnostic(reason);
|
||||
Self::add_non_alias_diagnostics(&mut diagnostic, causes);
|
||||
diagnostic::add_type_expression_reference_link(diagnostic)
|
||||
} else {
|
||||
builder.into_diagnostic(reason)
|
||||
};
|
||||
invalid.add_subdiagnostics(db, env, diagnostic, annotation);
|
||||
}
|
||||
error.fallback_type
|
||||
});
|
||||
self.check_type_variable_scope(annotation, result_ty)
|
||||
}
|
||||
|
||||
@@ -319,18 +355,64 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
&& definition
|
||||
.is_some_and(|definition| non_alias_assignment(self.db(), definition, ty).is_some())
|
||||
{
|
||||
self.report_invalid_type_expression(
|
||||
reference,
|
||||
format_args!(
|
||||
"Variable is not allowed in {}s",
|
||||
self.type_expression_context()
|
||||
),
|
||||
);
|
||||
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, reference) {
|
||||
let causes = self.find_non_alias_diagnostics(ty, definition);
|
||||
let mut diagnostic = builder
|
||||
.into_diagnostic(Self::invalid_variable_type_expression_message(reference));
|
||||
if let Some(causes) = causes {
|
||||
diagnostic.info(Self::invalid_type_alias_message(reference));
|
||||
Self::add_non_alias_diagnostics(&mut diagnostic, causes);
|
||||
}
|
||||
diagnostic::add_type_expression_reference_link(diagnostic);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn invalid_variable_type_expression_message(reference: &ast::Expr) -> String {
|
||||
UnqualifiedName::from_expr(reference).map_or_else(
|
||||
|| "Variable cannot be used as a type".to_owned(),
|
||||
|name| format!("Variable `{name}` cannot be used as a type"),
|
||||
)
|
||||
}
|
||||
|
||||
fn invalid_type_alias_message(reference: &ast::Expr) -> String {
|
||||
UnqualifiedName::from_expr(reference).map_or_else(
|
||||
|| "This variable is not a valid type alias".to_owned(),
|
||||
|name| format!("`{name}` is not a valid type alias"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Find diagnostics explaining why the referenced assignment is not a valid type alias.
|
||||
fn find_non_alias_diagnostics(
|
||||
&self,
|
||||
ty: Type<'db>,
|
||||
definition: Option<Definition<'db>>,
|
||||
) -> Option<&'db TypeCheckDiagnostics> {
|
||||
// Explain the original definition once, without recursively expanding errors from
|
||||
// other variables mentioned in its right-hand side.
|
||||
if self.context.is_type_expression_explanation()
|
||||
|| !self.context.is_lint_enabled(&INVALID_TYPE_FORM)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let definition = non_alias_assignment(self.db(), definition?, ty)?;
|
||||
let causes = non_alias_type_expression_diagnostics(self.db(), definition);
|
||||
(!causes.is_empty()).then_some(causes)
|
||||
}
|
||||
|
||||
/// Attach the errors from the invalid definition as notes at the use site.
|
||||
fn add_non_alias_diagnostics(diagnostic: &mut Diagnostic, causes: &TypeCheckDiagnostics) {
|
||||
for cause in causes {
|
||||
let mut sub = SubDiagnostic::new(SubDiagnosticSeverity::Info, cause.headline_message());
|
||||
for annotation in cause.annotations() {
|
||||
sub.annotate(annotation.clone());
|
||||
}
|
||||
diagnostic.sub(sub);
|
||||
}
|
||||
}
|
||||
|
||||
/// Infer the type of a type expression without storing the result.
|
||||
pub(super) fn infer_type_expression_no_store(&mut self, expression: &ast::Expr) -> Type<'db> {
|
||||
let db = self.db();
|
||||
@@ -640,8 +722,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.report_invalid_type_expression(
|
||||
expression,
|
||||
format_args!(
|
||||
"Invalid binary operator `{}` in type annotation",
|
||||
op.as_str()
|
||||
"Invalid binary operator `{}` in a {}",
|
||||
op.as_str(),
|
||||
self.type_expression_context()
|
||||
),
|
||||
);
|
||||
Type::unknown()
|
||||
|
||||
@@ -160,7 +160,8 @@ pub(super) fn legacy_type_alias_definition<'db>(
|
||||
/// Find the original assignment for a computed value that cannot be used as a type.
|
||||
///
|
||||
/// Callers use this to reject references that value-to-type conversion would otherwise accept,
|
||||
/// recovering with `Unknown` even when `invalid-type-form` is disabled.
|
||||
/// recovering with `Unknown` even when `invalid-type-form` is disabled. The returned definition
|
||||
/// also lets diagnostics explain the invalid right-hand side at its original source location.
|
||||
///
|
||||
/// A class-valued assignment is not necessarily a type declaration: `(lambda: int)()`
|
||||
/// produces the existing `int` class, whereas a recognized `NamedTuple` or `type` call
|
||||
|
||||
Reference in new issue
Block a user