[ty] split up types/infer.rs (#20342)
Some checks are pending
CI / cargo build (release) (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / mkdocs (push) Waiting to run
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

This commit is contained in:
Carl Meyer 2025-09-10 19:25:07 -07:00 committed by GitHub
parent a3ec8ca9df
commit c6b92b918e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 11818 additions and 11790 deletions

View file

@ -32,7 +32,7 @@ use crate::{
/// ## Consuming
/// It's important that the context is explicitly consumed before dropping by calling
/// [`InferContext::finish`] and the returned diagnostics must be stored
/// on the current [`TypeInferenceBuilder`](super::infer::TypeInferenceBuilder) result.
/// on the current inference result.
pub(crate) struct InferContext<'db, 'ast> {
db: &'db dyn Db,
scope: ScopeId<'db>,

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,284 @@
use ruff_python_ast as ast;
use super::{DeferredExpressionState, TypeInferenceBuilder};
use crate::types::diagnostic::{INVALID_TYPE_FORM, report_invalid_arguments_to_annotated};
use crate::types::string_annotation::{
BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation,
};
use crate::types::{
KnownClass, SpecialFormType, Type, TypeAndQualifiers, TypeQualifiers, todo_type,
};
/// Annotation expressions.
impl<'db> TypeInferenceBuilder<'db, '_> {
/// Infer the type of an annotation expression with the given [`DeferredExpressionState`].
pub(super) fn infer_annotation_expression(
&mut self,
annotation: &ast::Expr,
deferred_state: DeferredExpressionState,
) -> TypeAndQualifiers<'db> {
let previous_deferred_state = std::mem::replace(&mut self.deferred_state, deferred_state);
let annotation_ty = self.infer_annotation_expression_impl(annotation);
self.deferred_state = previous_deferred_state;
annotation_ty
}
/// Similar to [`infer_annotation_expression`], but accepts an optional annotation expression
/// and returns [`None`] if the annotation is [`None`].
///
/// [`infer_annotation_expression`]: TypeInferenceBuilder::infer_annotation_expression
pub(super) fn infer_optional_annotation_expression(
&mut self,
annotation: Option<&ast::Expr>,
deferred_state: DeferredExpressionState,
) -> Option<TypeAndQualifiers<'db>> {
annotation.map(|expr| self.infer_annotation_expression(expr, deferred_state))
}
/// Implementation of [`infer_annotation_expression`].
///
/// [`infer_annotation_expression`]: TypeInferenceBuilder::infer_annotation_expression
fn infer_annotation_expression_impl(
&mut self,
annotation: &ast::Expr,
) -> TypeAndQualifiers<'db> {
// https://typing.python.org/en/latest/spec/annotations.html#grammar-token-expression-grammar-annotation_expression
let annotation_ty = match annotation {
// String annotations: https://typing.python.org/en/latest/spec/annotations.html#string-annotations
ast::Expr::StringLiteral(string) => self.infer_string_annotation_expression(string),
// Annotation expressions also get special handling for `*args` and `**kwargs`.
ast::Expr::Starred(starred) => self.infer_starred_expression(starred).into(),
ast::Expr::BytesLiteral(bytes) => {
if let Some(builder) = self
.context
.report_lint(&BYTE_STRING_TYPE_ANNOTATION, bytes)
{
builder.into_diagnostic("Type expressions cannot use bytes literal");
}
TypeAndQualifiers::unknown()
}
ast::Expr::FString(fstring) => {
if let Some(builder) = self.context.report_lint(&FSTRING_TYPE_ANNOTATION, fstring) {
builder.into_diagnostic("Type expressions cannot use f-strings");
}
self.infer_fstring_expression(fstring);
TypeAndQualifiers::unknown()
}
ast::Expr::Name(name) => match name.ctx {
ast::ExprContext::Load => {
let name_expr_ty = self.infer_name_expression(name);
match name_expr_ty {
Type::SpecialForm(SpecialFormType::ClassVar) => {
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::CLASS_VAR)
}
Type::SpecialForm(SpecialFormType::Final) => {
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::FINAL)
}
Type::SpecialForm(SpecialFormType::Required) => {
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::REQUIRED)
}
Type::SpecialForm(SpecialFormType::NotRequired) => {
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::NOT_REQUIRED)
}
Type::SpecialForm(SpecialFormType::ReadOnly) => {
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::READ_ONLY)
}
Type::ClassLiteral(class)
if class.is_known(self.db(), KnownClass::InitVar) =>
{
if let Some(builder) =
self.context.report_lint(&INVALID_TYPE_FORM, annotation)
{
builder.into_diagnostic(
"`InitVar` may not be used without a type argument",
);
}
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::INIT_VAR)
}
_ => name_expr_ty
.in_type_expression(
self.db(),
self.scope(),
self.typevar_binding_context,
)
.unwrap_or_else(|error| {
error.into_fallback_type(
&self.context,
annotation,
self.is_reachable(annotation),
)
})
.into(),
}
}
ast::ExprContext::Invalid => TypeAndQualifiers::unknown(),
ast::ExprContext::Store | ast::ExprContext::Del => {
todo_type!("Name expression annotation in Store/Del context").into()
}
},
ast::Expr::Subscript(subscript @ ast::ExprSubscript { value, slice, .. }) => {
let value_ty = self.infer_expression(value);
let slice = &**slice;
match value_ty {
Type::SpecialForm(SpecialFormType::Annotated) => {
// This branch is similar to the corresponding branch in `infer_parameterized_special_form_type_expression`, but
// `Annotated[…]` can appear both in annotation expressions and in type expressions, and needs to be handled slightly
// differently in each case (calling either `infer_type_expression_*` or `infer_annotation_expression_*`).
if let ast::Expr::Tuple(ast::ExprTuple {
elts: arguments, ..
}) = slice
{
if arguments.len() < 2 {
report_invalid_arguments_to_annotated(&self.context, subscript);
}
if let [inner_annotation, metadata @ ..] = &arguments[..] {
for element in metadata {
self.infer_expression(element);
}
let inner_annotation_ty =
self.infer_annotation_expression_impl(inner_annotation);
self.store_expression_type(slice, inner_annotation_ty.inner_type());
inner_annotation_ty
} else {
for argument in arguments {
self.infer_expression(argument);
}
self.store_expression_type(slice, Type::unknown());
TypeAndQualifiers::unknown()
}
} else {
report_invalid_arguments_to_annotated(&self.context, subscript);
self.infer_annotation_expression_impl(slice)
}
}
Type::SpecialForm(
type_qualifier @ (SpecialFormType::ClassVar
| SpecialFormType::Final
| SpecialFormType::Required
| SpecialFormType::NotRequired
| SpecialFormType::ReadOnly),
) => {
let arguments = if let ast::Expr::Tuple(tuple) = slice {
&*tuple.elts
} else {
std::slice::from_ref(slice)
};
let num_arguments = arguments.len();
let type_and_qualifiers = if num_arguments == 1 {
let mut type_and_qualifiers =
self.infer_annotation_expression_impl(slice);
match type_qualifier {
SpecialFormType::ClassVar => {
type_and_qualifiers.add_qualifier(TypeQualifiers::CLASS_VAR);
}
SpecialFormType::Final => {
type_and_qualifiers.add_qualifier(TypeQualifiers::FINAL);
}
SpecialFormType::Required => {
type_and_qualifiers.add_qualifier(TypeQualifiers::REQUIRED);
}
SpecialFormType::NotRequired => {
type_and_qualifiers.add_qualifier(TypeQualifiers::NOT_REQUIRED);
}
SpecialFormType::ReadOnly => {
type_and_qualifiers.add_qualifier(TypeQualifiers::READ_ONLY);
}
_ => unreachable!(),
}
type_and_qualifiers
} else {
for element in arguments {
self.infer_annotation_expression_impl(element);
}
if let Some(builder) =
self.context.report_lint(&INVALID_TYPE_FORM, subscript)
{
builder.into_diagnostic(format_args!(
"Type qualifier `{type_qualifier}` expected exactly 1 argument, \
got {num_arguments}",
));
}
Type::unknown().into()
};
if slice.is_tuple_expr() {
self.store_expression_type(slice, type_and_qualifiers.inner_type());
}
type_and_qualifiers
}
Type::ClassLiteral(class) if class.is_known(self.db(), KnownClass::InitVar) => {
let arguments = if let ast::Expr::Tuple(tuple) = slice {
&*tuple.elts
} else {
std::slice::from_ref(slice)
};
let num_arguments = arguments.len();
let type_and_qualifiers = if num_arguments == 1 {
let mut type_and_qualifiers =
self.infer_annotation_expression_impl(slice);
type_and_qualifiers.add_qualifier(TypeQualifiers::INIT_VAR);
type_and_qualifiers
} else {
for element in arguments {
self.infer_annotation_expression_impl(element);
}
if let Some(builder) =
self.context.report_lint(&INVALID_TYPE_FORM, subscript)
{
builder.into_diagnostic(format_args!(
"Type qualifier `InitVar` expected exactly 1 argument, \
got {num_arguments}",
));
}
Type::unknown().into()
};
if slice.is_tuple_expr() {
self.store_expression_type(slice, type_and_qualifiers.inner_type());
}
type_and_qualifiers
}
_ => self
.infer_subscript_type_expression_no_store(subscript, slice, value_ty)
.into(),
}
}
// All other annotation expressions are (possibly) valid type expressions, so handle
// them there instead.
type_expr => self.infer_type_expression_no_store(type_expr).into(),
};
self.store_expression_type(annotation, annotation_ty.inner_type());
annotation_ty
}
/// Infer the type of a string annotation expression.
fn infer_string_annotation_expression(
&mut self,
string: &ast::ExprStringLiteral,
) -> TypeAndQualifiers<'db> {
match parse_string_annotation(&self.context, string) {
Some(parsed) => {
// String annotations are always evaluated in the deferred context.
self.infer_annotation_expression(
parsed.expr(),
DeferredExpressionState::InStringAnnotation(
self.enclosing_node_key(string.into()),
),
)
}
None => TypeAndQualifiers::unknown(),
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,641 @@
use super::builder::TypeInferenceBuilder;
use crate::db::tests::{TestDb, setup_db};
use crate::place::symbol;
use crate::place::{ConsideredDefinitions, Place, global_symbol};
use crate::semantic_index::definition::Definition;
use crate::semantic_index::scope::FileScopeId;
use crate::semantic_index::{global_scope, place_table, semantic_index, use_def_map};
use crate::types::{KnownInstanceType, check_types};
use ruff_db::diagnostic::Diagnostic;
use ruff_db::files::{File, system_path_to_file};
use ruff_db::system::DbWithWritableSystem as _;
use ruff_db::testing::{assert_function_query_was_not_run, assert_function_query_was_run};
use super::*;
#[track_caller]
fn get_symbol<'db>(
db: &'db TestDb,
file_name: &str,
scopes: &[&str],
symbol_name: &str,
) -> Place<'db> {
let file = system_path_to_file(db, file_name).expect("file to exist");
let module = parsed_module(db, file).load(db);
let index = semantic_index(db, file);
let mut file_scope_id = FileScopeId::global();
let mut scope = file_scope_id.to_scope_id(db, file);
for expected_scope_name in scopes {
file_scope_id = index
.child_scopes(file_scope_id)
.next()
.unwrap_or_else(|| panic!("scope of {expected_scope_name}"))
.0;
scope = file_scope_id.to_scope_id(db, file);
assert_eq!(scope.name(db, &module), *expected_scope_name);
}
symbol(db, scope, symbol_name, ConsideredDefinitions::EndOfScope).place
}
#[track_caller]
fn assert_diagnostic_messages(diagnostics: &[Diagnostic], expected: &[&str]) {
let messages: Vec<&str> = diagnostics
.iter()
.map(Diagnostic::primary_message)
.collect();
assert_eq!(&messages, expected);
}
#[track_caller]
fn assert_file_diagnostics(db: &TestDb, filename: &str, expected: &[&str]) {
let file = system_path_to_file(db, filename).unwrap();
let diagnostics = check_types(db, file);
assert_diagnostic_messages(&diagnostics, expected);
}
#[test]
fn not_literal_string() -> anyhow::Result<()> {
let mut db = setup_db();
let content = format!(
r#"
from typing_extensions import Literal, assert_type
assert_type(not "{y}", bool)
assert_type(not 10*"{y}", bool)
assert_type(not "{y}"*10, bool)
assert_type(not 0*"{y}", Literal[True])
assert_type(not (-100)*"{y}", Literal[True])
"#,
y = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1),
);
db.write_dedented("src/a.py", &content)?;
assert_file_diagnostics(&db, "src/a.py", &[]);
Ok(())
}
#[test]
fn multiplied_string() -> anyhow::Result<()> {
let mut db = setup_db();
let content = format!(
r#"
from typing_extensions import Literal, LiteralString, assert_type
assert_type(2 * "hello", Literal["hellohello"])
assert_type("goodbye" * 3, Literal["goodbyegoodbyegoodbye"])
assert_type("a" * {y}, Literal["{a_repeated}"])
assert_type({z} * "b", LiteralString)
assert_type(0 * "hello", Literal[""])
assert_type(-3 * "hello", Literal[""])
"#,
y = TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE,
z = TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1,
a_repeated = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE),
);
db.write_dedented("src/a.py", &content)?;
assert_file_diagnostics(&db, "src/a.py", &[]);
Ok(())
}
#[test]
fn multiplied_literal_string() -> anyhow::Result<()> {
let mut db = setup_db();
let content = format!(
r#"
from typing_extensions import Literal, LiteralString, assert_type
assert_type("{y}", LiteralString)
assert_type(10*"{y}", LiteralString)
assert_type("{y}"*10, LiteralString)
assert_type(0*"{y}", Literal[""])
assert_type((-100)*"{y}", Literal[""])
"#,
y = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1),
);
db.write_dedented("src/a.py", &content)?;
assert_file_diagnostics(&db, "src/a.py", &[]);
Ok(())
}
#[test]
fn truncated_string_literals_become_literal_string() -> anyhow::Result<()> {
let mut db = setup_db();
let content = format!(
r#"
from typing_extensions import LiteralString, assert_type
assert_type("{y}", LiteralString)
assert_type("a" + "{z}", LiteralString)
"#,
y = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1),
z = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE),
);
db.write_dedented("src/a.py", &content)?;
assert_file_diagnostics(&db, "src/a.py", &[]);
Ok(())
}
#[test]
fn adding_string_literals_and_literal_string() -> anyhow::Result<()> {
let mut db = setup_db();
let content = format!(
r#"
from typing_extensions import LiteralString, assert_type
assert_type("{y}", LiteralString)
assert_type("{y}" + "a", LiteralString)
assert_type("a" + "{y}", LiteralString)
assert_type("{y}" + "{y}", LiteralString)
"#,
y = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1),
);
db.write_dedented("src/a.py", &content)?;
assert_file_diagnostics(&db, "src/a.py", &[]);
Ok(())
}
#[test]
fn pep695_type_params() {
let mut db = setup_db();
db.write_dedented(
"src/a.py",
"
def f[T, U: A, V: (A, B), W = A, X: A = A1, Y: (int,)]():
pass
class A: ...
class B: ...
class A1(A): ...
",
)
.unwrap();
let check_typevar = |var: &'static str,
display: &'static str,
upper_bound: Option<&'static str>,
constraints: Option<&[&'static str]>,
default: Option<&'static str>| {
let var_ty = get_symbol(&db, "src/a.py", &["f"], var).expect_type();
assert_eq!(var_ty.display(&db).to_string(), display);
let expected_name_ty = format!(r#"Literal["{var}"]"#);
let name_ty = var_ty.member(&db, "__name__").place.expect_type();
assert_eq!(name_ty.display(&db).to_string(), expected_name_ty);
let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = var_ty else {
panic!("expected TypeVar");
};
assert_eq!(
typevar
.upper_bound(&db)
.map(|ty| ty.display(&db).to_string()),
upper_bound.map(std::borrow::ToOwned::to_owned)
);
assert_eq!(
typevar.constraints(&db).map(|tys| tys
.iter()
.map(|ty| ty.display(&db).to_string())
.collect::<Vec<_>>()),
constraints.map(|strings| strings
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>())
);
assert_eq!(
typevar
.default_type(&db)
.map(|ty| ty.display(&db).to_string()),
default.map(std::borrow::ToOwned::to_owned)
);
};
check_typevar("T", "typing.TypeVar", None, None, None);
check_typevar("U", "typing.TypeVar", Some("A"), None, None);
check_typevar("V", "typing.TypeVar", None, Some(&["A", "B"]), None);
check_typevar("W", "typing.TypeVar", None, None, Some("A"));
check_typevar("X", "typing.TypeVar", Some("A"), None, Some("A1"));
// a typevar with less than two constraints is treated as unconstrained
check_typevar("Y", "typing.TypeVar", None, None, None);
}
/// Test that a symbol known to be unbound in a scope does not still trigger cycle-causing
/// reachability-constraint checks in that scope.
#[test]
fn unbound_symbol_no_reachability_constraint_check() {
let mut db = setup_db();
// First, type-check a random other file so that we cache a result for the `module_type_symbols`
// query (which often encounters cycles due to `types.pyi` importing `typing_extensions` and
// `typing_extensions.pyi` importing `types`). Clear the events afterwards so that unrelated
// cycles from that query don't interfere with our test.
db.write_dedented("src/wherever.py", "print(x)").unwrap();
assert_file_diagnostics(&db, "src/wherever.py", &["Name `x` used when not defined"]);
db.clear_salsa_events();
// If the bug we are testing for is not fixed, what happens is that when inferring the
// `flag: bool = True` definitions, we look up `bool` as a deferred name (thus from end of
// scope), and because of the early return its "unbound" binding has a reachability
// constraint of `~flag`, which we evaluate, meaning we have to evaluate the definition of
// `flag` -- and we are in a cycle. With the fix, we short-circuit evaluating reachability
// constraints on "unbound" if a symbol is otherwise not bound.
db.write_dedented(
"src/a.py",
"
from __future__ import annotations
def f():
flag: bool = True
if flag:
return True
",
)
.unwrap();
db.clear_salsa_events();
assert_file_diagnostics(&db, "src/a.py", &[]);
let events = db.take_salsa_events();
let cycles = salsa::attach(&db, || {
events
.iter()
.filter_map(|event| {
if let salsa::EventKind::WillIterateCycle { database_key, .. } = event.kind {
Some(format!("{database_key:?}"))
} else {
None
}
})
.collect::<Vec<_>>()
});
let expected: Vec<String> = vec![];
assert_eq!(cycles, expected);
}
// Incremental inference tests
#[track_caller]
fn first_public_binding<'db>(db: &'db TestDb, file: File, name: &str) -> Definition<'db> {
let scope = global_scope(db, file);
use_def_map(db, scope)
.end_of_scope_symbol_bindings(place_table(db, scope).symbol_id(name).unwrap())
.find_map(|b| b.binding.definition())
.expect("no binding found")
}
#[test]
fn dependency_public_symbol_type_change() -> anyhow::Result<()> {
let mut db = setup_db();
db.write_files([
("/src/a.py", "from foo import x"),
("/src/foo.py", "x: int = 10\ndef foo(): ..."),
])?;
let a = system_path_to_file(&db, "/src/a.py").unwrap();
let x_ty = global_symbol(&db, a, "x").place.expect_type();
assert_eq!(x_ty.display(&db).to_string(), "int");
// Change `x` to a different value
db.write_file("/src/foo.py", "x: bool = True\ndef foo(): ...")?;
let a = system_path_to_file(&db, "/src/a.py").unwrap();
let x_ty_2 = global_symbol(&db, a, "x").place.expect_type();
assert_eq!(x_ty_2.display(&db).to_string(), "bool");
Ok(())
}
#[test]
fn dependency_internal_symbol_change() -> anyhow::Result<()> {
let mut db = setup_db();
db.write_files([
("/src/a.py", "from foo import x"),
("/src/foo.py", "x: int = 10\ndef foo(): y = 1"),
])?;
let a = system_path_to_file(&db, "/src/a.py").unwrap();
let x_ty = global_symbol(&db, a, "x").place.expect_type();
assert_eq!(x_ty.display(&db).to_string(), "int");
db.write_file("/src/foo.py", "x: int = 10\ndef foo(): pass")?;
let a = system_path_to_file(&db, "/src/a.py").unwrap();
db.clear_salsa_events();
let x_ty_2 = global_symbol(&db, a, "x").place.expect_type();
assert_eq!(x_ty_2.display(&db).to_string(), "int");
let events = db.take_salsa_events();
assert_function_query_was_not_run(
&db,
infer_definition_types,
first_public_binding(&db, a, "x"),
&events,
);
Ok(())
}
#[test]
fn dependency_unrelated_symbol() -> anyhow::Result<()> {
let mut db = setup_db();
db.write_files([
("/src/a.py", "from foo import x"),
("/src/foo.py", "x: int = 10\ny: bool = True"),
])?;
let a = system_path_to_file(&db, "/src/a.py").unwrap();
let x_ty = global_symbol(&db, a, "x").place.expect_type();
assert_eq!(x_ty.display(&db).to_string(), "int");
db.write_file("/src/foo.py", "x: int = 10\ny: bool = False")?;
let a = system_path_to_file(&db, "/src/a.py").unwrap();
db.clear_salsa_events();
let x_ty_2 = global_symbol(&db, a, "x").place.expect_type();
assert_eq!(x_ty_2.display(&db).to_string(), "int");
let events = db.take_salsa_events();
assert_function_query_was_not_run(
&db,
infer_definition_types,
first_public_binding(&db, a, "x"),
&events,
);
Ok(())
}
#[test]
fn dependency_implicit_instance_attribute() -> anyhow::Result<()> {
fn x_rhs_expression(db: &TestDb) -> Expression<'_> {
let file_main = system_path_to_file(db, "/src/main.py").unwrap();
let ast = parsed_module(db, file_main).load(db);
// Get the second statement in `main.py` (x = …) and extract the expression
// node on the right-hand side:
let x_rhs_node = &ast.syntax().body[1].as_assign_stmt().unwrap().value;
let index = semantic_index(db, file_main);
index.expression(x_rhs_node.as_ref())
}
let mut db = setup_db();
db.write_dedented(
"/src/mod.py",
r#"
class C:
def f(self):
self.attr: int | None = None
"#,
)?;
db.write_dedented(
"/src/main.py",
r#"
from mod import C
x = C().attr
"#,
)?;
let file_main = system_path_to_file(&db, "/src/main.py").unwrap();
let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | int | None");
// Change the type of `attr` to `str | None`; this should trigger the type of `x` to be re-inferred
db.write_dedented(
"/src/mod.py",
r#"
class C:
def f(self):
self.attr: str | None = None
"#,
)?;
let events = {
db.clear_salsa_events();
let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
db.take_salsa_events()
};
assert_function_query_was_run(&db, infer_expression_types, x_rhs_expression(&db), &events);
// Add a comment; this should not trigger the type of `x` to be re-inferred
db.write_dedented(
"/src/mod.py",
r#"
class C:
def f(self):
# a comment!
self.attr: str | None = None
"#,
)?;
let events = {
db.clear_salsa_events();
let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
db.take_salsa_events()
};
assert_function_query_was_not_run(&db, infer_expression_types, x_rhs_expression(&db), &events);
Ok(())
}
/// This test verifies that changing a class's declaration in a non-meaningful way (e.g. by adding a comment)
/// doesn't trigger type inference for expressions that depend on the class's members.
#[test]
fn dependency_own_instance_member() -> anyhow::Result<()> {
fn x_rhs_expression(db: &TestDb) -> Expression<'_> {
let file_main = system_path_to_file(db, "/src/main.py").unwrap();
let ast = parsed_module(db, file_main).load(db);
// Get the second statement in `main.py` (x = …) and extract the expression
// node on the right-hand side:
let x_rhs_node = &ast.syntax().body[1].as_assign_stmt().unwrap().value;
let index = semantic_index(db, file_main);
index.expression(x_rhs_node.as_ref())
}
let mut db = setup_db();
db.write_dedented(
"/src/mod.py",
r#"
class C:
if random.choice([True, False]):
attr: int = 42
else:
attr: None = None
"#,
)?;
db.write_dedented(
"/src/main.py",
r#"
from mod import C
x = C().attr
"#,
)?;
let file_main = system_path_to_file(&db, "/src/main.py").unwrap();
let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | int | None");
// Change the type of `attr` to `str | None`; this should trigger the type of `x` to be re-inferred
db.write_dedented(
"/src/mod.py",
r#"
class C:
if random.choice([True, False]):
attr: str = "42"
else:
attr: None = None
"#,
)?;
let events = {
db.clear_salsa_events();
let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
db.take_salsa_events()
};
assert_function_query_was_run(&db, infer_expression_types, x_rhs_expression(&db), &events);
// Add a comment; this should not trigger the type of `x` to be re-inferred
db.write_dedented(
"/src/mod.py",
r#"
class C:
# comment
if random.choice([True, False]):
attr: str = "42"
else:
attr: None = None
"#,
)?;
let events = {
db.clear_salsa_events();
let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
db.take_salsa_events()
};
assert_function_query_was_not_run(&db, infer_expression_types, x_rhs_expression(&db), &events);
Ok(())
}
#[test]
fn dependency_implicit_class_member() -> anyhow::Result<()> {
fn x_rhs_expression(db: &TestDb) -> Expression<'_> {
let file_main = system_path_to_file(db, "/src/main.py").unwrap();
let ast = parsed_module(db, file_main).load(db);
// Get the third statement in `main.py` (x = …) and extract the expression
// node on the right-hand side:
let x_rhs_node = &ast.syntax().body[2].as_assign_stmt().unwrap().value;
let index = semantic_index(db, file_main);
index.expression(x_rhs_node.as_ref())
}
let mut db = setup_db();
db.write_dedented(
"/src/mod.py",
r#"
class C:
def __init__(self):
self.instance_attr: str = "24"
@classmethod
def method(cls):
cls.class_attr: int = 42
"#,
)?;
db.write_dedented(
"/src/main.py",
r#"
from mod import C
C.method()
x = C().class_attr
"#,
)?;
let file_main = system_path_to_file(&db, "/src/main.py").unwrap();
let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | int");
// Change the type of `class_attr` to `str`; this should trigger the type of `x` to be re-inferred
db.write_dedented(
"/src/mod.py",
r#"
class C:
def __init__(self):
self.instance_attr: str = "24"
@classmethod
def method(cls):
cls.class_attr: str = "42"
"#,
)?;
let events = {
db.clear_salsa_events();
let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str");
db.take_salsa_events()
};
assert_function_query_was_run(&db, infer_expression_types, x_rhs_expression(&db), &events);
// Add a comment; this should not trigger the type of `x` to be re-inferred
db.write_dedented(
"/src/mod.py",
r#"
class C:
def __init__(self):
self.instance_attr: str = "24"
@classmethod
def method(cls):
# comment
cls.class_attr: str = "42"
"#,
)?;
let events = {
db.clear_salsa_events();
let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str");
db.take_salsa_events()
};
assert_function_query_was_not_run(&db, infer_expression_types, x_rhs_expression(&db), &events);
Ok(())
}

View file

@ -45,7 +45,7 @@ impl<'db> Mro<'db> {
/// the default specialization of the class's type variables.)
///
/// (We emit a diagnostic warning about the runtime `TypeError` in
/// [`super::infer::TypeInferenceBuilder::infer_region_scope`].)
/// [`super::infer::infer_scope_types`].)
pub(super) fn of_class(
db: &'db dyn Db,
class_literal: ClassLiteral<'db>,