mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[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
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:
parent
a3ec8ca9df
commit
c6b92b918e
7 changed files with 11818 additions and 11790 deletions
|
@ -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
9333
crates/ty_python_semantic/src/types/infer/builder.rs
Normal file
9333
crates/ty_python_semantic/src/types/infer/builder.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
1543
crates/ty_python_semantic/src/types/infer/builder/type_expression.rs
Normal file
1543
crates/ty_python_semantic/src/types/infer/builder/type_expression.rs
Normal file
File diff suppressed because it is too large
Load diff
641
crates/ty_python_semantic/src/types/infer/tests.rs
Normal file
641
crates/ty_python_semantic/src/types/infer/tests.rs
Normal 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(())
|
||||
}
|
|
@ -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>,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue