Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions crates/ruff_python_ast/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1681,6 +1681,14 @@ pub fn comment_indentation_after(
.unwrap_or_default()
}

pub fn is_dotted_name(expr: &ast::Expr) -> bool {
match expr {
ast::Expr::Name(_) => true,
ast::Expr::Attribute(ast::ExprAttribute { value, .. }) => is_dotted_name(value),
_ => false,
}
}

#[cfg(test)]
mod tests {
use std::borrow::Cow;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ def _(
## Invalid AST nodes

```py
from typing import TypeVar

T = TypeVar("T")

def bar() -> None:
return None

Expand Down Expand Up @@ -102,8 +106,12 @@ async def outer_async(): # avoid unrelated syntax errors on `yield` and `await`
# error: [unsupported-operator]
# error: [invalid-type-form] "F-strings are not allowed in type expressions"
p: int | f"foo",
# error: [invalid-type-form] "Invalid subscript"
# error: [invalid-type-form] "Only simple names and dotted names can be subscripted in type expressions"
q: [1, 2, 3][1:2],
# error: [invalid-type-form] "Only simple names and dotted names can be subscripted in type expressions"
r: list[T][int],
# error: [invalid-type-form] "Only simple names and dotted names can be subscripted in type expressions"
s: list[list[T][int]],
):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
Expand Down Expand Up @@ -276,8 +284,12 @@ async def outer_async(): # avoid unrelated syntax errors on `yield` and `await`
l: "(yield 1)", # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
m: "1 < 2", # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions"
n: "bar()", # error: [invalid-type-form] "Function calls are not allowed in type expressions"
# error: [invalid-type-form] "Invalid subscript"
# error: [invalid-type-form] "Only simple names and dotted names can be subscripted in type expressions"
o: "[1, 2, 3][1:2]",
# error: [invalid-type-form] "Only simple names, dotted names and subscripts can be used in type expressions"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wording of this message is a bit confusing, because the expression below could reasonably be said to contain only "simple names, dotted names, and subscripts". The subtlety is that "dotted names" can't include a subscript. Not sure how to make that subtlety clearer, though...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well... the expression below might contain only "simple names, dotted names and subscripts", but it is nonetheless not a simple name, dotted name or subscript itself! (It's a non-dotted-name attribute expression where the value is a subscript.)

But, uh, yeah, possibly not my finest error message... I also don't really have any better ideas, though. We do link to the typing spec grammar in a subdiagnostic, so that's something at least

p: list[int].append,
# error: [invalid-type-form] "Only simple names, dotted names and subscripts can be used in type expressions"
q: list[list[int].append],
):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
Expand All @@ -294,6 +306,8 @@ async def outer_async(): # avoid unrelated syntax errors on `yield` and `await`
reveal_type(m) # revealed: Unknown
reveal_type(n) # revealed: Unknown
reveal_type(o) # revealed: Unknown
reveal_type(p) # revealed: Unknown
reveal_type(q) # revealed: list[Unknown]
```

## Invalid Collection based AST nodes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,11 @@ def _(l: ListOfInts[int]):

type List[T] = list[T]

# error: [not-subscriptable] "Cannot specialize non-generic type alias: Double specialization is not allowed"
# error: [invalid-type-form] "Only simple names and dotted names can be subscripted in type expressions"
def _(l: List[int][int]):
reveal_type(l) # revealed: Unknown

# error: [not-subscriptable] "Cannot subscript non-generic type `<class 'list[T@DoubleSpecialization]'>`"
# error: [invalid-type-form] "Only simple names and dotted names can be subscripted in type expressions"
type DoubleSpecialization[T] = list[T][T]

def _(d: DoubleSpecialization[int]):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,7 @@ def this_does_not_work() -> TypeOf[IntOrStr]:
raise NotImplementedError()

def _(
# error: [not-subscriptable] "Cannot subscript non-generic type"
# error: [invalid-type-form] "Only simple names and dotted names can be subscripted in type expressions"
specialized: this_does_not_work()[int],
):
reveal_type(specialized) # revealed: Unknown
Expand All @@ -796,7 +796,7 @@ from typing import TypeVar

T = TypeVar("T")

# error: [not-subscriptable] "Cannot subscript non-generic type"
# error: [invalid-type-form] "Only simple names and dotted names can be subscripted in type expressions"
# error: [unbound-type-variable]
# error: [unbound-type-variable]
x: (list[T] | set[T])[int]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ python-version = "3.9"
from ty_extensions import is_singleton, static_assert

static_assert(is_singleton(Ellipsis.__class__))
static_assert(is_singleton((...).__class__))
```

### Python 3.10+
Expand Down
9 changes: 1 addition & 8 deletions crates/ty_python_semantic/src/semantic_index/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::sync::Arc;

use except_handlers::TryNodeContextStackManager;
use itertools::Itertools;
use ruff_python_ast::helpers::is_dotted_name;
use rustc_hash::{FxHashMap, FxHashSet};

use ruff_db::files::File;
Expand Down Expand Up @@ -3770,14 +3771,6 @@ impl ExpressionsScopeMapBuilder {

/// Returns if the expression is a `TYPE_CHECKING` expression.
fn is_if_type_checking(expr: &ast::Expr) -> bool {
fn is_dotted_name(expr: &ast::Expr) -> bool {
match expr {
ast::Expr::Name(_) => true,
ast::Expr::Attribute(ast::ExprAttribute { value, .. }) => is_dotted_name(value),
_ => false,
}
}

match expr {
ast::Expr::Name(ast::ExprName { id, .. }) => id == "TYPE_CHECKING",
ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => {
Expand Down
14 changes: 4 additions & 10 deletions crates/ty_python_semantic/src/types/infer/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity};
use ruff_db::files::File;
use ruff_db::parsed::ParsedModuleRef;
use ruff_db::source::source_text;
use ruff_python_ast::helpers::map_subscript;
use ruff_python_ast::helpers::{is_dotted_name, map_subscript};
use ruff_python_ast::name::Name;
use ruff_python_ast::{
self as ast, AnyNodeRef, ArgOrKeyword, ArgumentsSourceOrder, ExprContext, HasNodeIndex,
Expand Down Expand Up @@ -7438,7 +7438,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// and assume that it's actually `typing.reveal_type`.
let is_reveal_type = match func.as_ref() {
ast::Expr::Name(name) => name.id == "reveal_type",
ast::Expr::Attribute(attr) => attr.attr.id == "reveal_type",
ast::Expr::Attribute(attr) => {
attr.attr.id == "reveal_type" && is_dotted_name(func)
}
_ => false,
};
if is_reveal_type && let Some(first_arg) = arguments.args.first() {
Expand Down Expand Up @@ -8345,14 +8347,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {

/// Infer the type of a [`ast::ExprAttribute`] expression, assuming a load context.
fn infer_attribute_load(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> {
fn is_dotted_name(attribute: &ast::Expr) -> bool {
match attribute {
ast::Expr::Name(_) => true,
ast::Expr::Attribute(ast::ExprAttribute { value, .. }) => is_dotted_name(value),
_ => false,
}
}

fn union_elements_missing_attribute<'db>(
db: &'db dyn Db,
ty: Type<'db>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use ruff_python_ast as ast;
use ruff_python_ast::helpers::is_dotted_name;

use super::{DeferredExpressionState, TypeInferenceBuilder};
use crate::place::TypeOrigin;
Expand Down Expand Up @@ -188,22 +189,32 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
TypeAndQualifiers::declared(Type::unknown())
}

ast::Expr::Attribute(attribute) => match attribute.ctx {
ast::ExprContext::Load => {
let attribute_type = self.infer_attribute_expression(attribute);
if let Type::TypeVar(typevar) = attribute_type
&& typevar.paramspec_attr(self.db()).is_some()
{
TypeAndQualifiers::declared(attribute_type)
} else {
infer_name_or_attribute(attribute_type, annotation, self, pep_613_policy)
ast::Expr::Attribute(attribute) => {
if !is_dotted_name(annotation) {
return TypeAndQualifiers::declared(self.infer_type_expression(annotation));
}
match attribute.ctx {
ast::ExprContext::Load => {
let attribute_type = self.infer_attribute_expression(attribute);
if let Type::TypeVar(typevar) = attribute_type
&& typevar.paramspec_attr(self.db()).is_some()
{
TypeAndQualifiers::declared(attribute_type)
} else {
infer_name_or_attribute(
attribute_type,
annotation,
self,
pep_613_policy,
)
}
}
ast::ExprContext::Invalid => TypeAndQualifiers::declared(Type::unknown()),
ast::ExprContext::Store | ast::ExprContext::Del => TypeAndQualifiers::declared(
todo_type!("Attribute expression annotation in Store/Del context"),
),
}
ast::ExprContext::Invalid => TypeAndQualifiers::declared(Type::unknown()),
ast::ExprContext::Store | ast::ExprContext::Del => TypeAndQualifiers::declared(
todo_type!("Attribute expression annotation in Store/Del context"),
),
},
}

ast::Expr::Name(name) => match name.ctx {
ast::ExprContext::Load => infer_name_or_attribute(
Expand All @@ -219,9 +230,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
},

ast::Expr::Subscript(subscript @ ast::ExprSubscript { value, slice, .. }) => {
let value_ty = self.infer_expression(value, TypeContext::default());
if !is_dotted_name(value) {
return TypeAndQualifiers::declared(self.infer_type_expression(annotation));
}

let slice = &**slice;
let value_ty = self.infer_expression(value, TypeContext::default());

match value_ty {
Type::SpecialForm(special_form) => match special_form {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use itertools::Either;
use ruff_python_ast::helpers::is_dotted_name;
use ruff_python_ast::{self as ast, PythonVersion};

use super::{DeferredExpressionState, TypeInferenceBuilder};
Expand Down Expand Up @@ -99,22 +100,38 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
}
},

ast::Expr::Attribute(attribute_expression) => match attribute_expression.ctx {
ast::ExprContext::Load => self
.infer_attribute_expression(attribute_expression)
.default_specialize(self.db())
.in_type_expression(
self.db(),
self.scope(),
self.typevar_binding_context,
self.inference_flags,
)
.unwrap_or_else(|error| error.into_fallback_type(&self.context, expression)),
ast::ExprContext::Invalid => Type::unknown(),
ast::ExprContext::Store | ast::ExprContext::Del => {
todo_type!("Attribute expression annotation in Store/Del context")
ast::Expr::Attribute(attribute_expression) => {
if is_dotted_name(expression) {
match attribute_expression.ctx {
ast::ExprContext::Load => self
.infer_attribute_expression(attribute_expression)
.default_specialize(self.db())
.in_type_expression(
self.db(),
self.scope(),
self.typevar_binding_context,
self.inference_flags,
)
.unwrap_or_else(|error| {
error.into_fallback_type(&self.context, expression)
}),
ast::ExprContext::Invalid => Type::unknown(),
ast::ExprContext::Store | ast::ExprContext::Del => {
todo_type!("Attribute expression annotation in Store/Del context")
}
}
} else {
if !self.in_string_annotation() {
self.infer_attribute_expression(attribute_expression);
}
self.report_invalid_type_expression(
expression,
"Only simple names, dotted names and subscripts \
can be used in type expressions",
);
Type::unknown()
}
},
}

ast::Expr::NoneLiteral(_literal) => Type::none(self.db()),

Expand All @@ -132,7 +149,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> {

let value_ty = self.infer_expression(value, TypeContext::default());

self.infer_subscript_type_expression_no_store(subscript, slice, value_ty)
if is_dotted_name(value) {
self.infer_subscript_type_expression_no_store(subscript, slice, value_ty)
} else {
if !self.in_string_annotation() {
self.infer_expression(slice, TypeContext::default());
}
self.report_invalid_type_expression(
expression,
"Only simple names and dotted names can be subscripted in type expressions",
);
Type::unknown()
}
}

ast::Expr::BinOp(binary) => {
Expand Down
Loading