Skip to content

Commit

Permalink
Prototype of @no_type_check support
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaReiser committed Dec 23, 2024
1 parent 8d32708 commit f0ce890
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# `@no_type_check`

> If a type checker supports the `no_type_check` decorator for functions, it should suppress all type errors for the def statement and its body including any nested functions or classes. It should also ignore all parameter and return type annotations and treat the function as if it were unannotated.
> [source](https://typing.readthedocs.io/en/latest/spec/directives.html#no-type-check)
## Error in the function body

```py
from typing import no_type_check

@no_type_check
def test() -> int:
return a + 5
```

## Error in nested function

```py
from typing import no_type_check

@no_type_check
def test() -> int:
def nested():
return a + 5
```

## Error in nested class

```py
from typing import no_type_check

@no_type_check
def test() -> int:
class Nested:
def inner(self):
return a + 5
```

## Error in decorator

Both MyPy and Pyright flag the `unknown_decorator` but we don't.

```py
from typing import no_type_check

@unknown_decorator
@no_type_check
def test() -> int:
return a + 5
```

## Error in default value

```py
from typing import no_type_check

@no_type_check
def test(a: int = "test"):
return x + 5
```

## Error in return value position

```py
from typing import no_type_check

@no_type_check
def test() -> Undefined:
return x + 5
```

## `no_type_check` on classes isn't supported

Similar to Pyright, Red Knot does not support `no_type_check` annotations on classes.

```py
from typing import no_type_check

@no_type_check
class Test:
def test(self):
return a + 5 # error: [unresolved-reference]
```

## `type: ignore` comments in `@no_type_check` blocks

```py
from typing import no_type_check

@no_type_check
def test():
# error: [unused-ignore-comment]
return x + 5 # knot: ignore[unresolved-reference]
```
2 changes: 1 addition & 1 deletion crates/red_knot_python_semantic/src/ast_node_ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ impl<T> AstNodeRef<T> {
}

/// Returns a reference to the wrapped node.
pub fn node(&self) -> &T {
pub const fn node(&self) -> &T {
// SAFETY: Holding on to `parsed` ensures that the AST to which `node` belongs is still
// alive and not moved.
unsafe { self.node.as_ref() }
Expand Down
5 changes: 2 additions & 3 deletions crates/red_knot_python_semantic/src/semantic_index.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
use std::iter::FusedIterator;
use std::sync::Arc;

use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use salsa::plumbing::AsId;

use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
use ruff_index::{IndexSlice, IndexVec};
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use salsa::plumbing::AsId;

use crate::module_name::ModuleName;
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
Expand Down
13 changes: 6 additions & 7 deletions crates/red_knot_python_semantic/src/semantic_index/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ use ruff_db::parsed::ParsedModule;
use ruff_index::IndexVec;
use ruff_python_ast::name::Name;
use ruff_python_ast::visitor::{walk_expr, walk_pattern, walk_stmt, Visitor};
use ruff_python_ast::{self as ast, Pattern};
use ruff_python_ast::{BoolOp, Expr};
use ruff_python_ast::{self as ast};

use crate::ast_node_ref::AstNodeRef;
use crate::module_name::ModuleName;
Expand Down Expand Up @@ -289,7 +288,7 @@ impl<'db> SemanticIndexBuilder<'db> {
constraint
}

fn build_constraint(&mut self, constraint_node: &Expr) -> Constraint<'db> {
fn build_constraint(&mut self, constraint_node: &ast::Expr) -> Constraint<'db> {
let expression = self.add_standalone_expression(constraint_node);
Constraint {
node: ConstraintNode::Expression(expression),
Expand Down Expand Up @@ -408,11 +407,11 @@ impl<'db> SemanticIndexBuilder<'db> {
let guard = guard.map(|guard| self.add_standalone_expression(guard));

let kind = match pattern {
Pattern::MatchValue(pattern) => {
ast::Pattern::MatchValue(pattern) => {
let value = self.add_standalone_expression(&pattern.value);
PatternConstraintKind::Value(value, guard)
}
Pattern::MatchSingleton(singleton) => {
ast::Pattern::MatchSingleton(singleton) => {
PatternConstraintKind::Singleton(singleton.value, guard)
}
_ => PatternConstraintKind::Unsupported,
Expand Down Expand Up @@ -1492,8 +1491,8 @@ where
if index < values.len() - 1 {
let constraint = self.build_constraint(value);
let (constraint, constraint_id) = match op {
BoolOp::And => (constraint, self.add_constraint(constraint)),
BoolOp::Or => self.add_negated_constraint(constraint),
ast::BoolOp::And => (constraint, self.add_constraint(constraint)),
ast::BoolOp::Or => self.add_negated_constraint(constraint),
};
let visibility_constraint = self
.add_visibility_constraint(VisibilityConstraint::VisibleIf(constraint));
Expand Down
7 changes: 7 additions & 0 deletions crates/red_knot_python_semantic/src/semantic_index/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,13 @@ impl NodeWithScopeKind {
_ => panic!("expected type alias"),
}
}

pub const fn as_function(&self) -> Option<&ast::StmtFunctionDef> {
match self {
Self::Function(function) => Some(function.node()),
_ => None,
}
}
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
Expand Down
8 changes: 7 additions & 1 deletion crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3085,13 +3085,16 @@ pub enum KnownFunction {
Len,
/// `typing(_extensions).final`
Final,

/// [`typing(_extensions).no_type_check`](https://typing.readthedocs.io/en/latest/spec/directives.html#no-type-check)
NoTypeCheck,
}

impl KnownFunction {
pub fn constraint_function(self) -> Option<KnownConstraintFunction> {
match self {
Self::ConstraintFunction(f) => Some(f),
Self::RevealType | Self::Len | Self::Final => None,
Self::RevealType | Self::Len | Self::Final | Self::NoTypeCheck => None,
}
}

Expand All @@ -3110,6 +3113,9 @@ impl KnownFunction {
),
"len" if definition.is_builtin_definition(db) => Some(KnownFunction::Len),
"final" if definition.is_typing_definition(db) => Some(KnownFunction::Final),
"no_type_check" if definition.is_typing_definition(db) => {
Some(KnownFunction::NoTypeCheck)
}
_ => None,
}
}
Expand Down
68 changes: 63 additions & 5 deletions crates/red_knot_python_semantic/src/types/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ use ruff_db::{
files::File,
};
use ruff_python_ast::AnyNodeRef;
use ruff_text_size::Ranged;
use ruff_text_size::{Ranged, TextRange};

use super::{binding_ty, KnownFunction, TypeCheckDiagnostic, TypeCheckDiagnostics};

use crate::semantic_index::semantic_index;
use crate::semantic_index::symbol::ScopeId;
use crate::{
lint::{LintId, LintMetadata},
suppression::suppressions,
Db,
};

use super::{TypeCheckDiagnostic, TypeCheckDiagnostics};

/// Context for inferring the types of a single file.
///
/// One context exists for at least for every inferred region but it's
Expand All @@ -30,16 +32,18 @@ use super::{TypeCheckDiagnostic, TypeCheckDiagnostics};
/// on the current [`TypeInference`](super::infer::TypeInference) result.
pub(crate) struct InferContext<'db> {
db: &'db dyn Db,
scope: ScopeId<'db>,
file: File,
diagnostics: std::cell::RefCell<TypeCheckDiagnostics>,
bomb: DebugDropBomb,
}

impl<'db> InferContext<'db> {
pub(crate) fn new(db: &'db dyn Db, file: File) -> Self {
pub(crate) fn new(db: &'db dyn Db, scope: ScopeId<'db>) -> Self {
Self {
db,
file,
scope,
file: scope.file(db),
diagnostics: std::cell::RefCell::new(TypeCheckDiagnostics::default()),
bomb: DebugDropBomb::new("`InferContext` needs to be explicitly consumed by calling `::finish` to prevent accidental loss of diagnostics."),
}
Expand Down Expand Up @@ -68,11 +72,19 @@ impl<'db> InferContext<'db> {
node: AnyNodeRef,
message: fmt::Arguments,
) {
if !self.db.is_file_open(self.file) {
return;
}

// Skip over diagnostics if the rule is disabled.
let Some(severity) = self.db.rule_selection().severity(LintId::of(lint)) else {
return;
};

if self.is_in_no_type_check(node.range()) {
return;
}

let suppressions = suppressions(self.db, self.file);

if let Some(suppression) = suppressions.find_suppression(node.range(), LintId::of(lint)) {
Expand Down Expand Up @@ -112,6 +124,52 @@ impl<'db> InferContext<'db> {
});
}

fn is_in_no_type_check(&self, range: TextRange) -> bool {
// Accessing the semantic index here is fine because
// the index belongs to the same file as for which we emit the diagnostic.
let index = semantic_index(self.db, self.file);

let scope_id = self.scope.file_scope_id(self.db);

// Unfortunately, we can't just use the `scope_id` here because the default values, return type
// and other parts of a function declaration are inferred in the outer scope, and not in the function's scope.
// That's why we walk all child-scopes to see if there's any child scope that fully contains the diagnostic range
// and if there's any, use that scope as the starting scope instead.
// We could probably use a binary search here but it's probably not worth it, considering that most
// scopes have only very few child scopes and binary search also isn't free.
let enclosing_scope = index
.child_scopes(scope_id)
.find_map(|(child_scope_id, scope)| {
if scope
.node()
.as_function()
.is_some_and(|function| function.range().contains_range(range))
{
Some(child_scope_id)
} else {
None
}
})
.unwrap_or(scope_id);

// Inspect all enclosing function scopes walking bottom up and infer the function's type.
let mut function_scope_tys = index
.ancestor_scopes(enclosing_scope)
.filter_map(|(_, scope)| scope.node().as_function())
.filter_map(|function| {
binding_ty(self.db, index.definition(function)).into_function_literal()
});

// Iterate over all functions and test if any is decorated with `@no_type_check`.
function_scope_tys.any(|function_ty| {
function_ty
.decorators(self.db)
.iter()
.filter_map(|decorator| decorator.into_function_literal())
.any(|decorator_ty| decorator_ty.is_known(self.db, KnownFunction::NoTypeCheck))
})
}

#[must_use]
pub(crate) fn finish(mut self) -> TypeCheckDiagnostics {
self.bomb.defuse();
Expand Down
Loading

0 comments on commit f0ce890

Please sign in to comment.