diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 178fb687ba9914..759e5754ef2326 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -8,7 +8,7 @@ Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -49,7 +49,7 @@ class Derived(Base): # Error: `Derived` does not implement `method` Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -90,7 +90,7 @@ class SubProto(BaseProto, Protocol): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -157,7 +157,7 @@ def test(): -> "int": Default level: error · Preview (since 0.0.16) · Related issues · -View source +View source @@ -206,7 +206,7 @@ Foo.method() # Error: cannot call abstract classmethod Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -230,7 +230,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.7 · Related issues · -View source +View source @@ -261,7 +261,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -293,7 +293,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -324,7 +324,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -356,7 +356,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -388,7 +388,7 @@ class B(A): ... Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -416,7 +416,7 @@ type B = A Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -448,7 +448,7 @@ class Example: Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -475,7 +475,7 @@ old_func() # emits [deprecated] diagnostic Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -504,7 +504,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -531,7 +531,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -569,7 +569,7 @@ class A: # Crash at runtime Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -634,13 +634,45 @@ Static analysis tools like ty can't analyze type annotations that contain escape def foo() -> "intt\b": ... ``` +## `final-on-non-method` + + +Default level: error · +Added in 0.0.20 · +Related issues · +View source + + + +**What it does** + +Checks for `@final` decorators applied to non-method functions. + +**Why is this bad?** + +The `@final` decorator is only meaningful on methods and classes. +Applying it to a module-level function or a nested function has no +effect and is likely a mistake. + +**Example** + + +```python +from typing import final + +# Error: @final is not allowed on non-method functions +@final +def my_function() -> int: + return 0 +``` + ## `final-without-value` Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -766,7 +798,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -796,7 +828,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -822,7 +854,7 @@ t[3] # IndexError: tuple index out of range Default level: warn · Added in 0.0.1-alpha.33 · Related issues · -View source +View source @@ -856,7 +888,7 @@ class MyClass: ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -945,7 +977,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -972,7 +1004,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1000,7 +1032,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1034,7 +1066,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1070,7 +1102,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1094,7 +1126,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1121,7 +1153,7 @@ with 1: Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1158,7 +1190,7 @@ class Foo(NamedTuple): Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -1190,7 +1222,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1219,7 +1251,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1263,7 +1295,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -1305,7 +1337,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -1349,7 +1381,7 @@ class NonFrozenChild(FrozenBase): # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1387,7 +1419,7 @@ class D(Generic[U, T]): ... Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1466,7 +1498,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -1505,7 +1537,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: warn · Added in 0.0.15 · Related issues · -View source +View source @@ -1566,7 +1598,7 @@ def f(x, y, /): # Python 3.8+ syntax Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1601,7 +1633,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.18 · Related issues · -View source +View source @@ -1629,7 +1661,7 @@ match x: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1663,7 +1695,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1770,7 +1802,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule. Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1824,7 +1856,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Added in 0.0.1-alpha.27 · Related issues · -View source +View source @@ -1854,7 +1886,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1904,7 +1936,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1930,7 +1962,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1961,7 +1993,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1995,7 +2027,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2044,7 +2076,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2073,7 +2105,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2169,7 +2201,7 @@ class C: ... Default level: error · Added in 0.0.10 · Related issues · -View source +View source @@ -2215,7 +2247,7 @@ class MyClass: Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -2242,7 +2274,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2289,7 +2321,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2319,7 +2351,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2349,7 +2381,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2383,7 +2415,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2417,7 +2449,7 @@ class C: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2448,7 +2480,7 @@ def g[U, T: U](): ... # error: [invalid-type-variable-bound] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2495,7 +2527,7 @@ U = TypeVar('U', list[int], int) # valid constrained Type Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2527,7 +2559,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2562,7 +2594,7 @@ def f(x: dict): Default level: error · Added in 0.0.9 · Related issues · -View source +View source @@ -2593,7 +2625,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2648,7 +2680,7 @@ def h(arg2: type): Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2691,7 +2723,7 @@ def g(arg: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2716,7 +2748,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2749,7 +2781,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2778,7 +2810,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2804,7 +2836,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2828,7 +2860,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2861,7 +2893,7 @@ class B(A): Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -2894,7 +2926,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2921,7 +2953,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2948,7 +2980,7 @@ f(x=1) # Error raised here Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2976,7 +3008,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3008,7 +3040,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3045,7 +3077,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3109,7 +3141,7 @@ def test(): -> "int": Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3136,7 +3168,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.18 · Related issues · -View source +View source @@ -3168,7 +3200,7 @@ class C: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3198,7 +3230,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3227,7 +3259,7 @@ class B(A): ... # Error raised here Default level: error · Added in 0.0.1-alpha.30 · Related issues · -View source +View source @@ -3261,7 +3293,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3288,7 +3320,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3316,7 +3348,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3362,7 +3394,7 @@ class A: Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3386,7 +3418,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3413,7 +3445,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3441,7 +3473,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -3499,7 +3531,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3524,7 +3556,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3549,7 +3581,7 @@ print(x) # NameError: name 'x' is not defined Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -3588,7 +3620,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3625,7 +3657,7 @@ b1 < b2 < b1 # exception raised here Default level: ignore · Added in 0.0.12 · Related issues · -View source +View source @@ -3666,7 +3698,7 @@ def factory(base: type[Base]) -> type: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3767,7 +3799,7 @@ to `false`. Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3830,7 +3862,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/mdtest/final.md b/crates/ty_python_semantic/resources/mdtest/final.md index 1b6081908f339b..78149aabcd1bc0 100644 --- a/crates/ty_python_semantic/resources/mdtest/final.md +++ b/crates/ty_python_semantic/resources/mdtest/final.md @@ -428,6 +428,29 @@ class D(B): # error: [subclass-of-final-class] def method(self): ... # error: [override-of-final-variable] ``` +## `@final` cannot be applied to non-method functions + +The `@final` decorator is only valid on methods and classes. Using it on a module-level or nested +function is an error. + +```py +from typing import final + +@final # error: [final-on-non-method] "`@final` cannot be applied to non-method function `func1`" +def func1(): ... + +# Nested function decorated with `@final` is also invalid +def outer(): + @final # error: [final-on-non-method] + def inner(): ... + +# A function nested inside a method is also not a method +class F: + def method(self): + @final # error: [final-on-non-method] + def not_a_method(): ... +``` + ## An `@final` method is overridden by an implicit instance attribute ```py diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 6fcfec2fcc7d43..0485f2fad1d400 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -119,6 +119,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&OVERRIDE_OF_FINAL_METHOD); registry.register_lint(&OVERRIDE_OF_FINAL_VARIABLE); registry.register_lint(&INEFFECTIVE_FINAL); + registry.register_lint(&FINAL_ON_NON_METHOD); registry.register_lint(&FINAL_WITHOUT_VALUE); registry.register_lint(&ABSTRACT_METHOD_IN_FINAL_CLASS); registry.register_lint(&CALL_ABSTRACT_METHOD); @@ -2162,6 +2163,32 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for `@final` decorators applied to non-method functions. + /// + /// ## Why is this bad? + /// The `@final` decorator is only meaningful on methods and classes. + /// Applying it to a module-level function or a nested function has no + /// effect and is likely a mistake. + /// + /// ## Example + /// + /// ```python + /// from typing import final + /// + /// # Error: @final is not allowed on non-method functions + /// @final + /// def my_function() -> int: + /// return 0 + /// ``` + pub(crate) static FINAL_ON_NON_METHOD = { + summary: "detects `@final` applied to non-method functions", + status: LintStatus::stable("0.0.20"), + default_level: Level::Error, + } +} + declare_lint! { /// ## What it does /// Checks for `Final` symbols that are declared without a value and are never diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index cc66d51609d0e3..8d456ef5a410a8 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -73,21 +73,21 @@ use crate::types::diagnostic::{ self, ABSTRACT_METHOD_IN_FINAL_CLASS, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DATACLASS_FIELD_ORDER, DIVISION_BY_ZERO, DUPLICATE_BASE, DUPLICATE_KW_ONLY, - FINAL_WITHOUT_VALUE, INCONSISTENT_MRO, INEFFECTIVE_FINAL, INVALID_ARGUMENT_TYPE, - INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DATACLASS, - INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_GENERIC_ENUM, INVALID_KEY, - INVALID_LEGACY_POSITIONAL_PARAMETER, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, - INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, - INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_ARGUMENTS, - INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_GUARD_DEFINITION, - INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPE_VARIABLE_DEFAULT, - INVALID_TYPED_DICT_HEADER, INVALID_TYPED_DICT_STATEMENT, IncompatibleBases, MISSING_ARGUMENT, - NO_MATCHING_OVERLOAD, NOT_SUBSCRIPTABLE, PARAMETER_ALREADY_ASSIGNED, - POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, - SUBCLASS_OF_FINAL_CLASS, TOO_MANY_POSITIONAL_ARGUMENTS, TypedDictDeleteErrorKind, - UNDEFINED_REVEAL, UNKNOWN_ARGUMENT, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, - UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, - hint_if_stdlib_attribute_exists_on_other_versions, + FINAL_ON_NON_METHOD, FINAL_WITHOUT_VALUE, INCONSISTENT_MRO, INEFFECTIVE_FINAL, + INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, + INVALID_DATACLASS, INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_GENERIC_ENUM, + INVALID_KEY, INVALID_LEGACY_POSITIONAL_PARAMETER, INVALID_LEGACY_TYPE_VARIABLE, + INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, + INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ALIAS_TYPE, + INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, + INVALID_TYPE_GUARD_DEFINITION, INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, + INVALID_TYPE_VARIABLE_DEFAULT, INVALID_TYPED_DICT_HEADER, INVALID_TYPED_DICT_STATEMENT, + IncompatibleBases, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD, NOT_SUBSCRIPTABLE, + PARAMETER_ALREADY_ASSIGNED, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, + POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, TOO_MANY_POSITIONAL_ARGUMENTS, + TypedDictDeleteErrorKind, UNDEFINED_REVEAL, UNKNOWN_ARGUMENT, UNRESOLVED_ATTRIBUTE, + UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, + UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions, hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance, report_call_to_abstract_method, report_cannot_delete_typed_dict_key, @@ -3186,6 +3186,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut function_decorators = FunctionDecorators::empty(); let mut deprecated = None; let mut dataclass_transformer_params = None; + let mut final_decorator = None; for decorator in decorator_list { let decorator_type = self.infer_decorator(decorator); @@ -3194,14 +3195,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { function_decorators |= decorator_function_decorator; match decorator_type { - Type::FunctionLiteral(function) => { - if let Some(KnownFunction::NoTypeCheck) = function.known(self.db()) { + Type::FunctionLiteral(function) => match function.known(self.db()) { + Some(KnownFunction::NoTypeCheck) => { // If the function is decorated with the `no_type_check` decorator, // we need to suppress any errors that come after the decorators. self.context.set_in_no_type_check(InNoTypeCheck::Yes); continue; } - } + Some(KnownFunction::Final) => { + final_decorator = Some(decorator); + continue; + } + _ => {} + }, Type::KnownInstance(KnownInstanceType::Deprecated(deprecated_inst)) => { deprecated = Some(deprecated_inst); } @@ -3217,6 +3223,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { decorator_types_and_nodes.push((decorator_type, decorator)); } + // Check for `@final` applied to non-method functions. + // `@final` is only meaningful on methods and classes. + if let Some(final_decorator) = final_decorator + && !self + .index + .scope(self.scope().file_scope_id(self.db())) + .kind() + .is_class() + && let Some(builder) = self + .context + .report_lint(&FINAL_ON_NON_METHOD, final_decorator) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "`@final` cannot be applied to non-method function `{name}`", + )); + diagnostic.info("`@final` is only meaningful on methods and classes"); + } + let has_defaults = parameters .iter_non_variadic_params() .any(|param| param.default.is_some()); diff --git a/ty.schema.json b/ty.schema.json index cd9f861c8c7a3d..ff954b6d68d2ab 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -555,6 +555,16 @@ } ] }, + "final-on-non-method": { + "title": "detects `@final` applied to non-method functions", + "description": "## What it does\nChecks for `@final` decorators applied to non-method functions.\n\n## Why is this bad?\nThe `@final` decorator is only meaningful on methods and classes.\nApplying it to a module-level function or a nested function has no\neffect and is likely a mistake.\n\n## Example\n\n```python\nfrom typing import final\n\n# Error: @final is not allowed on non-method functions\n@final\ndef my_function() -> int:\n return 0\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "final-without-value": { "title": "detects `Final` declarations without a value", "description": "## What it does\nChecks for `Final` symbols that are declared without a value and are never\nassigned a value in their scope.\n\n## Why is this bad?\nA `Final` symbol must be initialized with a value at the time of declaration\nor in a subsequent assignment. At module or function scope, the assignment must\noccur in the same scope. In a class body, the assignment may occur in `__init__`.\n\n## Examples\n```python\nfrom typing import Final\n\n# Error: `Final` symbol without a value\nMY_CONSTANT: Final[int]\n\n# OK: `Final` symbol with a value\nMY_CONSTANT: Final[int] = 1\n```",