Skip to content

Commit 0caab81

Browse files
MichaReisercarljm
andauthored
@no_type_check support (#15122)
Co-authored-by: Carl Meyer <[email protected]>
1 parent d4ee6ab commit 0caab81

File tree

8 files changed

+253
-47
lines changed

8 files changed

+253
-47
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# `@no_type_check`
2+
3+
> If a type checker supports the `no_type_check` decorator for functions, it should suppress all
4+
> type errors for the def statement and its body including any nested functions or classes. It
5+
> should also ignore all parameter and return type annotations and treat the function as if it were
6+
> unannotated. [source](https://typing.readthedocs.io/en/latest/spec/directives.html#no-type-check)
7+
8+
## Error in the function body
9+
10+
```py
11+
from typing import no_type_check
12+
13+
@no_type_check
14+
def test() -> int:
15+
return a + 5
16+
```
17+
18+
## Error in nested function
19+
20+
```py
21+
from typing import no_type_check
22+
23+
@no_type_check
24+
def test() -> int:
25+
def nested():
26+
return a + 5
27+
```
28+
29+
## Error in nested class
30+
31+
```py
32+
from typing import no_type_check
33+
34+
@no_type_check
35+
def test() -> int:
36+
class Nested:
37+
def inner(self):
38+
return a + 5
39+
```
40+
41+
## Error in preceding decorator
42+
43+
Don't suppress diagnostics for decorators appearing before the `no_type_check` decorator.
44+
45+
```py
46+
from typing import no_type_check
47+
48+
@unknown_decorator # error: [unresolved-reference]
49+
@no_type_check
50+
def test() -> int:
51+
return a + 5
52+
```
53+
54+
## Error in following decorator
55+
56+
Unlike Pyright and mypy, suppress diagnostics appearing after the `no_type_check` decorator. We do
57+
this because it more closely matches Python's runtime semantics of decorators. For more details, see
58+
the discussion on the
59+
[PR adding `@no_type_check` support](https://github.com/astral-sh/ruff/pull/15122#discussion_r1896869411).
60+
61+
```py
62+
from typing import no_type_check
63+
64+
@no_type_check
65+
@unknown_decorator
66+
def test() -> int:
67+
return a + 5
68+
```
69+
70+
## Error in default value
71+
72+
```py
73+
from typing import no_type_check
74+
75+
@no_type_check
76+
def test(a: int = "test"):
77+
return x + 5
78+
```
79+
80+
## Error in return value position
81+
82+
```py
83+
from typing import no_type_check
84+
85+
@no_type_check
86+
def test() -> Undefined:
87+
return x + 5
88+
```
89+
90+
## `no_type_check` on classes isn't supported
91+
92+
Red Knot does not support decorating classes with `no_type_check`. The behaviour of `no_type_check`
93+
when applied to classes is
94+
[not specified currently](https://typing.readthedocs.io/en/latest/spec/directives.html#no-type-check),
95+
and is not supported by Pyright or mypy.
96+
97+
A future improvement might be to emit a diagnostic if a `no_type_check` annotation is applied to a
98+
class.
99+
100+
```py
101+
from typing import no_type_check
102+
103+
@no_type_check
104+
class Test:
105+
def test(self):
106+
return a + 5 # error: [unresolved-reference]
107+
```
108+
109+
## `type: ignore` comments in `@no_type_check` blocks
110+
111+
```py
112+
from typing import no_type_check
113+
114+
@no_type_check
115+
def test():
116+
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unresolved-reference'"
117+
return x + 5 # knot: ignore[unresolved-reference]
118+
```

crates/red_knot_python_semantic/src/ast_node_ref.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ impl<T> AstNodeRef<T> {
4343
}
4444

4545
/// Returns a reference to the wrapped node.
46-
pub fn node(&self) -> &T {
46+
pub const fn node(&self) -> &T {
4747
// SAFETY: Holding on to `parsed` ensures that the AST to which `node` belongs is still
4848
// alive and not moved.
4949
unsafe { self.node.as_ref() }

crates/red_knot_python_semantic/src/semantic_index/builder.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@ use rustc_hash::{FxHashMap, FxHashSet};
66
use ruff_db::files::File;
77
use ruff_db::parsed::ParsedModule;
88
use ruff_index::IndexVec;
9+
use ruff_python_ast as ast;
910
use ruff_python_ast::name::Name;
1011
use ruff_python_ast::visitor::{walk_expr, walk_pattern, walk_stmt, Visitor};
11-
use ruff_python_ast::{self as ast, Pattern};
12-
use ruff_python_ast::{BoolOp, Expr};
1312

1413
use crate::ast_node_ref::AstNodeRef;
1514
use crate::module_name::ModuleName;
@@ -289,7 +288,7 @@ impl<'db> SemanticIndexBuilder<'db> {
289288
constraint
290289
}
291290

292-
fn build_constraint(&mut self, constraint_node: &Expr) -> Constraint<'db> {
291+
fn build_constraint(&mut self, constraint_node: &ast::Expr) -> Constraint<'db> {
293292
let expression = self.add_standalone_expression(constraint_node);
294293
Constraint {
295294
node: ConstraintNode::Expression(expression),
@@ -408,11 +407,11 @@ impl<'db> SemanticIndexBuilder<'db> {
408407
let guard = guard.map(|guard| self.add_standalone_expression(guard));
409408

410409
let kind = match pattern {
411-
Pattern::MatchValue(pattern) => {
410+
ast::Pattern::MatchValue(pattern) => {
412411
let value = self.add_standalone_expression(&pattern.value);
413412
PatternConstraintKind::Value(value, guard)
414413
}
415-
Pattern::MatchSingleton(singleton) => {
414+
ast::Pattern::MatchSingleton(singleton) => {
416415
PatternConstraintKind::Singleton(singleton.value, guard)
417416
}
418417
_ => PatternConstraintKind::Unsupported,
@@ -1492,8 +1491,8 @@ where
14921491
if index < values.len() - 1 {
14931492
let constraint = self.build_constraint(value);
14941493
let (constraint, constraint_id) = match op {
1495-
BoolOp::And => (constraint, self.add_constraint(constraint)),
1496-
BoolOp::Or => self.add_negated_constraint(constraint),
1494+
ast::BoolOp::And => (constraint, self.add_constraint(constraint)),
1495+
ast::BoolOp::Or => self.add_negated_constraint(constraint),
14971496
};
14981497
let visibility_constraint = self
14991498
.add_visibility_constraint(VisibilityConstraint::VisibleIf(constraint));

crates/red_knot_python_semantic/src/semantic_index/symbol.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -463,10 +463,7 @@ impl NodeWithScopeKind {
463463
}
464464

465465
pub fn expect_function(&self) -> &ast::StmtFunctionDef {
466-
match self {
467-
Self::Function(function) => function.node(),
468-
_ => panic!("expected function"),
469-
}
466+
self.as_function().expect("expected function")
470467
}
471468

472469
pub fn expect_type_alias(&self) -> &ast::StmtTypeAlias {
@@ -475,6 +472,13 @@ impl NodeWithScopeKind {
475472
_ => panic!("expected type alias"),
476473
}
477474
}
475+
476+
pub const fn as_function(&self) -> Option<&ast::StmtFunctionDef> {
477+
match self {
478+
Self::Function(function) => Some(function.node()),
479+
_ => None,
480+
}
481+
}
478482
}
479483

480484
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]

crates/red_knot_python_semantic/src/types.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3086,13 +3086,16 @@ pub enum KnownFunction {
30863086
Len,
30873087
/// `typing(_extensions).final`
30883088
Final,
3089+
3090+
/// [`typing(_extensions).no_type_check`](https://typing.readthedocs.io/en/latest/spec/directives.html#no-type-check)
3091+
NoTypeCheck,
30893092
}
30903093

30913094
impl KnownFunction {
30923095
pub fn constraint_function(self) -> Option<KnownConstraintFunction> {
30933096
match self {
30943097
Self::ConstraintFunction(f) => Some(f),
3095-
Self::RevealType | Self::Len | Self::Final => None,
3098+
Self::RevealType | Self::Len | Self::Final | Self::NoTypeCheck => None,
30963099
}
30973100
}
30983101

@@ -3111,6 +3114,9 @@ impl KnownFunction {
31113114
),
31123115
"len" if definition.is_builtin_definition(db) => Some(KnownFunction::Len),
31133116
"final" if definition.is_typing_definition(db) => Some(KnownFunction::Final),
3117+
"no_type_check" if definition.is_typing_definition(db) => {
3118+
Some(KnownFunction::NoTypeCheck)
3119+
}
31143120
_ => None,
31153121
}
31163122
}

crates/red_knot_python_semantic/src/types/context.rs

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@ use ruff_db::{
88
use ruff_python_ast::AnyNodeRef;
99
use ruff_text_size::Ranged;
1010

11+
use super::{binding_ty, KnownFunction, TypeCheckDiagnostic, TypeCheckDiagnostics};
12+
13+
use crate::semantic_index::semantic_index;
14+
use crate::semantic_index::symbol::ScopeId;
1115
use crate::{
1216
lint::{LintId, LintMetadata},
1317
suppression::suppressions,
1418
Db,
1519
};
1620

17-
use super::{TypeCheckDiagnostic, TypeCheckDiagnostics};
18-
1921
/// Context for inferring the types of a single file.
2022
///
2123
/// One context exists for at least for every inferred region but it's
@@ -30,17 +32,21 @@ use super::{TypeCheckDiagnostic, TypeCheckDiagnostics};
3032
/// on the current [`TypeInference`](super::infer::TypeInference) result.
3133
pub(crate) struct InferContext<'db> {
3234
db: &'db dyn Db,
35+
scope: ScopeId<'db>,
3336
file: File,
3437
diagnostics: std::cell::RefCell<TypeCheckDiagnostics>,
38+
no_type_check: InNoTypeCheck,
3539
bomb: DebugDropBomb,
3640
}
3741

3842
impl<'db> InferContext<'db> {
39-
pub(crate) fn new(db: &'db dyn Db, file: File) -> Self {
43+
pub(crate) fn new(db: &'db dyn Db, scope: ScopeId<'db>) -> Self {
4044
Self {
4145
db,
42-
file,
46+
scope,
47+
file: scope.file(db),
4348
diagnostics: std::cell::RefCell::new(TypeCheckDiagnostics::default()),
49+
no_type_check: InNoTypeCheck::default(),
4450
bomb: DebugDropBomb::new("`InferContext` needs to be explicitly consumed by calling `::finish` to prevent accidental loss of diagnostics."),
4551
}
4652
}
@@ -68,11 +74,19 @@ impl<'db> InferContext<'db> {
6874
node: AnyNodeRef,
6975
message: fmt::Arguments,
7076
) {
77+
if !self.db.is_file_open(self.file) {
78+
return;
79+
}
80+
7181
// Skip over diagnostics if the rule is disabled.
7282
let Some(severity) = self.db.rule_selection().severity(LintId::of(lint)) else {
7383
return;
7484
};
7585

86+
if self.is_in_no_type_check() {
87+
return;
88+
}
89+
7690
let suppressions = suppressions(self.db, self.file);
7791

7892
if let Some(suppression) = suppressions.find_suppression(node.range(), LintId::of(lint)) {
@@ -112,6 +126,42 @@ impl<'db> InferContext<'db> {
112126
});
113127
}
114128

129+
pub(super) fn set_in_no_type_check(&mut self, no_type_check: InNoTypeCheck) {
130+
self.no_type_check = no_type_check;
131+
}
132+
133+
fn is_in_no_type_check(&self) -> bool {
134+
match self.no_type_check {
135+
InNoTypeCheck::Possibly => {
136+
// Accessing the semantic index here is fine because
137+
// the index belongs to the same file as for which we emit the diagnostic.
138+
let index = semantic_index(self.db, self.file);
139+
140+
let scope_id = self.scope.file_scope_id(self.db);
141+
142+
// Inspect all ancestor function scopes by walking bottom up and infer the function's type.
143+
let mut function_scope_tys = index
144+
.ancestor_scopes(scope_id)
145+
.filter_map(|(_, scope)| scope.node().as_function())
146+
.filter_map(|function| {
147+
binding_ty(self.db, index.definition(function)).into_function_literal()
148+
});
149+
150+
// Iterate over all functions and test if any is decorated with `@no_type_check`.
151+
function_scope_tys.any(|function_ty| {
152+
function_ty
153+
.decorators(self.db)
154+
.iter()
155+
.filter_map(|decorator| decorator.into_function_literal())
156+
.any(|decorator_ty| {
157+
decorator_ty.is_known(self.db, KnownFunction::NoTypeCheck)
158+
})
159+
})
160+
}
161+
InNoTypeCheck::Yes => true,
162+
}
163+
}
164+
115165
#[must_use]
116166
pub(crate) fn finish(mut self) -> TypeCheckDiagnostics {
117167
self.bomb.defuse();
@@ -131,6 +181,17 @@ impl fmt::Debug for InferContext<'_> {
131181
}
132182
}
133183

184+
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
185+
pub(crate) enum InNoTypeCheck {
186+
/// The inference might be in a `no_type_check` block but only if any
187+
/// ancestor function is decorated with `@no_type_check`.
188+
#[default]
189+
Possibly,
190+
191+
/// The inference is known to be in an `@no_type_check` decorated function.
192+
Yes,
193+
}
194+
134195
pub(crate) trait WithDiagnostics {
135196
fn diagnostics(&self) -> &TypeCheckDiagnostics;
136197
}

0 commit comments

Comments
 (0)