diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/generic_alias.md b/crates/ty_python_semantic/resources/mdtest/annotations/generic_alias.md index e1036281de..7235262be5 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/generic_alias.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/generic_alias.md @@ -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 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md index 1f6a8fcdf7..652460726b 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md @@ -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 diff --git a/crates/ty_python_semantic/resources/mdtest/cycle/basic.md b/crates/ty_python_semantic/resources/mdtest/cycle/basic.md index ea738fbd4e..1b70800fea 100644 --- a/crates/ty_python_semantic/resources/mdtest/cycle/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/cycle/basic.md @@ -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 diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md index 7d51b886db..04ed3c2d77 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md @@ -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) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md index 7a2ac864cc..0977b86483 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md @@ -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]): diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index efb379aa85..2eb27be057 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -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: diff --git a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md index 0a9022a350..19ea20c1a2 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -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 diff --git a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md index 47fe414390..12276287c5 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -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 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_Validating_`ParamSpe…_(327594c6dacd8ad).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_Validating_`ParamSpe…_(327594c6dacd8ad).snap index 170d8af1e6..57346171a0 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_Validating_`ParamSpe…_(327594c6dacd8ad).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_Validating_`ParamSpe…_(327594c6dacd8ad).snap @@ -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 diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 800467a65f..e53ad6d14c 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -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> { diff --git a/crates/ty_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs index 447605d920..45b5c4ed37 100644 --- a/crates/ty_python_semantic/src/types/context.rs +++ b/crates/ty_python_semantic/src/types/context.rs @@ -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, has_invalid_type_form: Cell, - 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> { 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> { - if ctx.diagnostics_suppressed { + if ctx.diagnostic_mode != DiagnosticMode::Normal { return None; } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 7e4e2567ae..cc37439499 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -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" } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index e03517df0f..97067ac166 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -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 diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 2c8f04df7d..1436f735ce 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -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>, + ) -> 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() diff --git a/crates/ty_python_semantic/src/types/infer/legacy_type_alias.rs b/crates/ty_python_semantic/src/types/infer/legacy_type_alias.rs index 88b070ae93..0564b88946 100644 --- a/crates/ty_python_semantic/src/types/infer/legacy_type_alias.rs +++ b/crates/ty_python_semantic/src/types/infer/legacy_type_alias.rs @@ -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