diff --git a/crates/ty_project/resources/test/corpus/sub_exprs_not_found_in_evaluate_expr_compare.py b/crates/ty_project/resources/test/corpus/sub_exprs_not_found_in_evaluate_expr_compare.py new file mode 100644 index 0000000000000..7dc644bec95f2 --- /dev/null +++ b/crates/ty_project/resources/test/corpus/sub_exprs_not_found_in_evaluate_expr_compare.py @@ -0,0 +1,11 @@ +# This is a regression test for `infer_expression_types`. +# ref: https://github.com/astral-sh/ruff/pull/18041#discussion_r2094573989 + +class C: + def f(self, other: "C"): + if self.a > other.b or self.b: + return False + if self: + return True + +C().a diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 0b74ab50aa3c8..71288e5109343 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -37,7 +37,9 @@ reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown # See https://github.com/astral-sh/ruff/issues/15960 for a related discussion. reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None -reveal_type(c_instance.declared_only) # revealed: bytes +# TODO: Should be `bytes` with no error, like mypy and pyright? +# error: [unresolved-attribute] +reveal_type(c_instance.declared_only) # revealed: Unknown reveal_type(c_instance.declared_and_bound) # revealed: bool @@ -64,12 +66,10 @@ C.inferred_from_value = "overwritten on class" # This assignment is fine: c_instance.declared_and_bound = False -# TODO: After this assignment to the attribute within this scope, we may eventually want to narrow -# the `bool` type (see above) for this instance variable to `Literal[False]` here. This is unsound -# in general (we don't know what else happened to `c_instance` between the assignment and the use -# here), but mypy and pyright support this. In conclusion, this could be `bool` but should probably -# be `Literal[False]`. -reveal_type(c_instance.declared_and_bound) # revealed: bool +# Strictly speaking, inferring this as `Literal[False]` rather than `bool` is unsound in general +# (we don't know what else happened to `c_instance` between the assignment and the use here), +# but mypy and pyright support this. +reveal_type(c_instance.declared_and_bound) # revealed: Literal[False] ``` #### Variable declared in class body and possibly bound in `__init__` @@ -149,14 +149,16 @@ class C: c_instance = C(True) reveal_type(c_instance.only_declared_in_body) # revealed: str | None -reveal_type(c_instance.only_declared_in_init) # revealed: str | None +# TODO: should be `str | None` without error +# error: [unresolved-attribute] +reveal_type(c_instance.only_declared_in_init) # revealed: Unknown reveal_type(c_instance.declared_in_body_and_init) # revealed: str | None reveal_type(c_instance.declared_in_body_defined_in_init) # revealed: str | None # TODO: This should be `str | None`. Fixing this requires an overhaul of the `Symbol` API, # which is planned in https://github.com/astral-sh/ruff/issues/14297 -reveal_type(c_instance.bound_in_body_declared_in_init) # revealed: Unknown | str | None +reveal_type(c_instance.bound_in_body_declared_in_init) # revealed: Unknown | Literal["a"] reveal_type(c_instance.bound_in_body_and_init) # revealed: Unknown | None | Literal["a"] ``` @@ -187,7 +189,9 @@ reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None -reveal_type(c_instance.declared_only) # revealed: bytes +# TODO: should be `bytes` with no error, like mypy and pyright? +# error: [unresolved-attribute] +reveal_type(c_instance.declared_only) # revealed: Unknown reveal_type(c_instance.declared_and_bound) # revealed: bool @@ -260,8 +264,8 @@ class C: self.w += None # TODO: Mypy and pyright do not support this, but it would be great if we could -# infer `Unknown | str` or at least `Unknown | Weird | str` here. -reveal_type(C().w) # revealed: Unknown | Weird +# infer `Unknown | str` here (`Weird` is not a possible type for the `w` attribute). +reveal_type(C().w) # revealed: Unknown ``` #### Attributes defined in tuple unpackings @@ -410,14 +414,41 @@ class C: [... for self.a in IntIterable()] [... for (self.b, self.c) in TupleIterable()] [... for self.d in IntIterable() for self.e in IntIterable()] + [[... for self.f in IntIterable()] for _ in IntIterable()] + [[... for self.g in IntIterable()] for self in [D()]] + +class D: + g: int c_instance = C() -reveal_type(c_instance.a) # revealed: Unknown | int -reveal_type(c_instance.b) # revealed: Unknown | int -reveal_type(c_instance.c) # revealed: Unknown | str -reveal_type(c_instance.d) # revealed: Unknown | int -reveal_type(c_instance.e) # revealed: Unknown | int +# TODO: no error, reveal Unknown | int +# error: [unresolved-attribute] +reveal_type(c_instance.a) # revealed: Unknown + +# TODO: no error, reveal Unknown | int +# error: [unresolved-attribute] +reveal_type(c_instance.b) # revealed: Unknown + +# TODO: no error, reveal Unknown | str +# error: [unresolved-attribute] +reveal_type(c_instance.c) # revealed: Unknown + +# TODO: no error, reveal Unknown | int +# error: [unresolved-attribute] +reveal_type(c_instance.d) # revealed: Unknown + +# TODO: no error, reveal Unknown | int +# error: [unresolved-attribute] +reveal_type(c_instance.e) # revealed: Unknown + +# TODO: no error, reveal Unknown | int +# error: [unresolved-attribute] +reveal_type(c_instance.f) # revealed: Unknown + +# This one is correctly not resolved as an attribute: +# error: [unresolved-attribute] +reveal_type(c_instance.g) # revealed: Unknown ``` #### Conditionally declared / bound attributes @@ -721,10 +752,7 @@ reveal_type(C.pure_class_variable) # revealed: Unknown # error: [invalid-attribute-access] "Cannot assign to instance attribute `pure_class_variable` from the class object ``" C.pure_class_variable = "overwritten on class" -# TODO: should be `Unknown | Literal["value set in class method"]` or -# Literal["overwritten on class"]`, once/if we support local narrowing. -# error: [unresolved-attribute] -reveal_type(C.pure_class_variable) # revealed: Unknown +reveal_type(C.pure_class_variable) # revealed: Literal["overwritten on class"] c_instance = C() reveal_type(c_instance.pure_class_variable) # revealed: Unknown | Literal["value set in class method"] @@ -762,19 +790,12 @@ reveal_type(c_instance.variable_with_class_default2) # revealed: Unknown | Lite c_instance.variable_with_class_default1 = "value set on instance" reveal_type(C.variable_with_class_default1) # revealed: str - -# TODO: Could be Literal["value set on instance"], or still `str` if we choose not to -# narrow the type. -reveal_type(c_instance.variable_with_class_default1) # revealed: str +reveal_type(c_instance.variable_with_class_default1) # revealed: Literal["value set on instance"] C.variable_with_class_default1 = "overwritten on class" -# TODO: Could be `Literal["overwritten on class"]`, or still `str` if we choose not to -# narrow the type. -reveal_type(C.variable_with_class_default1) # revealed: str - -# TODO: should still be `Literal["value set on instance"]`, or `str`. -reveal_type(c_instance.variable_with_class_default1) # revealed: str +reveal_type(C.variable_with_class_default1) # revealed: Literal["overwritten on class"] +reveal_type(c_instance.variable_with_class_default1) # revealed: Literal["value set on instance"] ``` #### Descriptor attributes as class variables diff --git a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index cc9661b54f6a8..86b16e4202673 100644 --- a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -699,9 +699,7 @@ class C: descriptor = Descriptor() C.descriptor = "something else" - -# This could also be `Literal["something else"]` if we support narrowing of attribute types based on assignments -reveal_type(C.descriptor) # revealed: Unknown | int +reveal_type(C.descriptor) # revealed: Literal["something else"] ``` ### Possibly unbound descriptor attributes diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md b/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md new file mode 100644 index 0000000000000..73d676a2a371b --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md @@ -0,0 +1,318 @@ +# Narrowing by assignment + +## Attribute + +### Basic + +```py +class A: + x: int | None = None + y = None + + def __init__(self): + self.z = None + +a = A() +a.x = 0 +a.y = 0 +a.z = 0 + +reveal_type(a.x) # revealed: Literal[0] +reveal_type(a.y) # revealed: Literal[0] +reveal_type(a.z) # revealed: Literal[0] + +# Make sure that we infer the narrowed type for eager +# scopes (class, comprehension) and the non-narrowed +# public type for lazy scopes (function) +class _: + reveal_type(a.x) # revealed: Literal[0] + reveal_type(a.y) # revealed: Literal[0] + reveal_type(a.z) # revealed: Literal[0] + +[reveal_type(a.x) for _ in range(1)] # revealed: Literal[0] +[reveal_type(a.y) for _ in range(1)] # revealed: Literal[0] +[reveal_type(a.z) for _ in range(1)] # revealed: Literal[0] + +def _(): + reveal_type(a.x) # revealed: Unknown | int | None + reveal_type(a.y) # revealed: Unknown | None + reveal_type(a.z) # revealed: Unknown | None + +if False: + a = A() +reveal_type(a.x) # revealed: Literal[0] +reveal_type(a.y) # revealed: Literal[0] +reveal_type(a.z) # revealed: Literal[0] + +if True: + a = A() +reveal_type(a.x) # revealed: int | None +reveal_type(a.y) # revealed: Unknown | None +reveal_type(a.z) # revealed: Unknown | None + +a.x = 0 +a.y = 0 +a.z = 0 +reveal_type(a.x) # revealed: Literal[0] +reveal_type(a.y) # revealed: Literal[0] +reveal_type(a.z) # revealed: Literal[0] + +class _: + a = A() + reveal_type(a.x) # revealed: int | None + reveal_type(a.y) # revealed: Unknown | None + reveal_type(a.z) # revealed: Unknown | None + +def cond() -> bool: + return True + +class _: + if False: + a = A() + reveal_type(a.x) # revealed: Literal[0] + reveal_type(a.y) # revealed: Literal[0] + reveal_type(a.z) # revealed: Literal[0] + + if cond(): + a = A() + reveal_type(a.x) # revealed: int | None + reveal_type(a.y) # revealed: Unknown | None + reveal_type(a.z) # revealed: Unknown | None + +class _: + a = A() + + class Inner: + reveal_type(a.x) # revealed: int | None + reveal_type(a.y) # revealed: Unknown | None + reveal_type(a.z) # revealed: Unknown | None + +# error: [unresolved-reference] +does.nt.exist = 0 +# error: [unresolved-reference] +reveal_type(does.nt.exist) # revealed: Unknown +``` + +### Narrowing chain + +```py +class D: ... + +class C: + d: D | None = None + +class B: + c1: C | None = None + c2: C | None = None + +class A: + b: B | None = None + +a = A() +a.b = B() +a.b.c1 = C() +a.b.c2 = C() +a.b.c1.d = D() +a.b.c2.d = D() +reveal_type(a.b) # revealed: B +reveal_type(a.b.c1) # revealed: C +reveal_type(a.b.c1.d) # revealed: D + +a.b.c1 = C() +reveal_type(a.b) # revealed: B +reveal_type(a.b.c1) # revealed: C +reveal_type(a.b.c1.d) # revealed: D | None +reveal_type(a.b.c2.d) # revealed: D + +a.b.c1.d = D() +a.b = B() +reveal_type(a.b) # revealed: B +reveal_type(a.b.c1) # revealed: C | None +reveal_type(a.b.c2) # revealed: C | None +# error: [possibly-unbound-attribute] +reveal_type(a.b.c1.d) # revealed: D | None +# error: [possibly-unbound-attribute] +reveal_type(a.b.c2.d) # revealed: D | None +``` + +### Do not narrow the type of a `property` by assignment + +```py +class C: + def __init__(self): + self._x: int = 0 + + @property + def x(self) -> int: + return self._x + + @x.setter + def x(self, value: int) -> None: + self._x = abs(value) + +c = C() +c.x = -1 +# Don't infer `c.x` to be `Literal[-1]` +reveal_type(c.x) # revealed: int +``` + +### Do not narrow the type of a descriptor by assignment + +```py +class Descriptor: + def __get__(self, instance: object, owner: type) -> int: + return 1 + + def __set__(self, instance: object, value: int) -> None: + pass + +class C: + desc: Descriptor = Descriptor() + +c = C() +c.desc = -1 +# Don't infer `c.desc` to be `Literal[-1]` +reveal_type(c.desc) # revealed: int +``` + +## Subscript + +### Specialization for builtin types + +Type narrowing based on assignment to a subscript expression is generally unsound, because arbitrary +`__getitem__`/`__setitem__` methods on a class do not necessarily guarantee that the passed-in value +for `__setitem__` is stored and can be retrieved unmodified via `__getitem__`. Therefore, we +currently only perform assignment-based narrowing on a few built-in classes (`list`, `dict`, +`bytesarray`, `TypedDict` and `collections` types) where we are confident that this kind of +narrowing can be performed soundly. This is the same approach as pyright. + +```py +from typing import TypedDict +from collections import ChainMap, defaultdict + +l: list[int | None] = [None] +l[0] = 0 +d: dict[int, int] = {1: 1} +d[0] = 0 +b: bytearray = bytearray(b"abc") +b[0] = 0 +dd: defaultdict[int, int] = defaultdict(int) +dd[0] = 0 +cm: ChainMap[int, int] = ChainMap({1: 1}, {0: 0}) +cm[0] = 0 +# TODO: should be ChainMap[int, int] +reveal_type(cm) # revealed: ChainMap[Unknown, Unknown] + +reveal_type(l[0]) # revealed: Literal[0] +reveal_type(d[0]) # revealed: Literal[0] +reveal_type(b[0]) # revealed: Literal[0] +reveal_type(dd[0]) # revealed: Literal[0] +# TODO: should be Literal[0] +reveal_type(cm[0]) # revealed: Unknown + +class C: + reveal_type(l[0]) # revealed: Literal[0] + reveal_type(d[0]) # revealed: Literal[0] + reveal_type(b[0]) # revealed: Literal[0] + reveal_type(dd[0]) # revealed: Literal[0] + # TODO: should be Literal[0] + reveal_type(cm[0]) # revealed: Unknown + +[reveal_type(l[0]) for _ in range(1)] # revealed: Literal[0] +[reveal_type(d[0]) for _ in range(1)] # revealed: Literal[0] +[reveal_type(b[0]) for _ in range(1)] # revealed: Literal[0] +[reveal_type(dd[0]) for _ in range(1)] # revealed: Literal[0] +# TODO: should be Literal[0] +[reveal_type(cm[0]) for _ in range(1)] # revealed: Unknown + +def _(): + reveal_type(l[0]) # revealed: int | None + reveal_type(d[0]) # revealed: int + reveal_type(b[0]) # revealed: int + reveal_type(dd[0]) # revealed: int + reveal_type(cm[0]) # revealed: int + +class D(TypedDict): + x: int + label: str + +td = D(x=1, label="a") +td["x"] = 0 +# TODO: should be Literal[0] +reveal_type(td["x"]) # revealed: @Todo(TypedDict) + +# error: [unresolved-reference] +does["not"]["exist"] = 0 +# error: [unresolved-reference] +reveal_type(does["not"]["exist"]) # revealed: Unknown + +non_subscriptable = 1 +# error: [non-subscriptable] +non_subscriptable[0] = 0 +# error: [non-subscriptable] +reveal_type(non_subscriptable[0]) # revealed: Unknown +``` + +### No narrowing for custom classes with arbitrary `__getitem__` / `__setitem__` + +```py +class C: + def __init__(self): + self.l: list[str] = [] + + def __getitem__(self, index: int) -> str: + return self.l[index] + + def __setitem__(self, index: int, value: str | int) -> None: + if len(self.l) == index: + self.l.append(str(value)) + else: + self.l[index] = str(value) + +c = C() +c[0] = 0 +reveal_type(c[0]) # revealed: str +``` + +## Complex target + +```py +class A: + x: list[int | None] = [] + +class B: + a: A | None = None + +b = B() +b.a = A() +b.a.x[0] = 0 + +reveal_type(b.a.x[0]) # revealed: Literal[0] + +class C: + reveal_type(b.a.x[0]) # revealed: Literal[0] + +def _(): + # error: [possibly-unbound-attribute] + reveal_type(b.a.x[0]) # revealed: Unknown | int | None + # error: [possibly-unbound-attribute] + reveal_type(b.a.x) # revealed: Unknown | list[int | None] + reveal_type(b.a) # revealed: Unknown | A | None +``` + +## Invalid assignments are not used for narrowing + +```py +class C: + x: int | None + l: list[int] + +def f(c: C, s: str): + c.x = s # error: [invalid-assignment] + reveal_type(c.x) # revealed: int | None + s = c.x # error: [invalid-assignment] + + # TODO: This assignment is invalid and should result in an error. + c.l[0] = s + reveal_type(c.l[0]) # revealed: int +``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md index 6b001ea09411a..b3b077f1bcb71 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md @@ -53,11 +53,114 @@ constraints may no longer be valid due to a "time lag". However, it may be possi that some of them are valid by performing a more detailed analysis (e.g. checking that the narrowing target has not changed in all places where the function is called). +### Narrowing by attribute/subscript assignments + +```py +class A: + x: str | None = None + + def update_x(self, value: str | None): + self.x = value + +a = A() +a.x = "a" + +class B: + reveal_type(a.x) # revealed: Literal["a"] + +def f(): + reveal_type(a.x) # revealed: Unknown | str | None + +[reveal_type(a.x) for _ in range(1)] # revealed: Literal["a"] + +a = A() + +class C: + reveal_type(a.x) # revealed: str | None + +def g(): + reveal_type(a.x) # revealed: Unknown | str | None + +[reveal_type(a.x) for _ in range(1)] # revealed: str | None + +a = A() +a.x = "a" +a.update_x("b") + +class D: + # TODO: should be `str | None` + reveal_type(a.x) # revealed: Literal["a"] + +def h(): + reveal_type(a.x) # revealed: Unknown | str | None + +# TODO: should be `str | None` +[reveal_type(a.x) for _ in range(1)] # revealed: Literal["a"] +``` + +### Narrowing by attribute/subscript assignments in nested scopes + +```py +class D: ... + +class C: + d: D | None = None + +class B: + c1: C | None = None + c2: C | None = None + +class A: + b: B | None = None + +a = A() +a.b = B() + +class _: + a.b.c1 = C() + + class _: + a.b.c1.d = D() + a = 1 + + class _3: + reveal_type(a) # revealed: A + reveal_type(a.b.c1.d) # revealed: D + + class _: + a = 1 + # error: [unresolved-attribute] + a.b.c1.d = D() + + class _3: + reveal_type(a) # revealed: A + # TODO: should be `D | None` + reveal_type(a.b.c1.d) # revealed: D + +a.b.c1 = C() +a.b.c1.d = D() + +class _: + a.b = B() + + class _: + # error: [possibly-unbound-attribute] + reveal_type(a.b.c1.d) # revealed: D | None + reveal_type(a.b.c1) # revealed: C | None +``` + ### Narrowing constraints introduced in eager nested scopes ```py g: str | None = "a" +class A: + x: str | None = None + +a = A() + +l: list[str | None] = [None] + def f(x: str | None): def _(): if x is not None: @@ -69,6 +172,14 @@ def f(x: str | None): if g is not None: reveal_type(g) # revealed: str + if a.x is not None: + # TODO(#17643): should be `Unknown | str` + reveal_type(a.x) # revealed: Unknown | str | None + + if l[0] is not None: + # TODO(#17643): should be `str` + reveal_type(l[0]) # revealed: str | None + class C: if x is not None: reveal_type(x) # revealed: str @@ -79,6 +190,14 @@ def f(x: str | None): if g is not None: reveal_type(g) # revealed: str + if a.x is not None: + # TODO(#17643): should be `Unknown | str` + reveal_type(a.x) # revealed: Unknown | str | None + + if l[0] is not None: + # TODO(#17643): should be `str` + reveal_type(l[0]) # revealed: str | None + # TODO: should be str # This could be fixed if we supported narrowing with if clauses in comprehensions. [reveal_type(x) for _ in range(1) if x is not None] # revealed: str | None @@ -89,6 +208,13 @@ def f(x: str | None): ```py g: str | None = "a" +class A: + x: str | None = None + +a = A() + +l: list[str | None] = [None] + def f(x: str | None): if x is not None: def _(): @@ -109,6 +235,28 @@ def f(x: str | None): reveal_type(g) # revealed: str [reveal_type(g) for _ in range(1)] # revealed: str + + if a.x is not None: + def _(): + reveal_type(a.x) # revealed: Unknown | str | None + + class D: + # TODO(#17643): should be `Unknown | str` + reveal_type(a.x) # revealed: Unknown | str | None + + # TODO(#17643): should be `Unknown | str` + [reveal_type(a.x) for _ in range(1)] # revealed: Unknown | str | None + + if l[0] is not None: + def _(): + reveal_type(l[0]) # revealed: str | None + + class D: + # TODO(#17643): should be `str` + reveal_type(l[0]) # revealed: str | None + + # TODO(#17643): should be `str` + [reveal_type(l[0]) for _ in range(1)] # revealed: str | None ``` ### Narrowing constraints introduced in multiple scopes @@ -118,6 +266,13 @@ from typing import Literal g: str | Literal[1] | None = "a" +class A: + x: str | Literal[1] | None = None + +a = A() + +l: list[str | Literal[1] | None] = [None] + def f(x: str | Literal[1] | None): class C: if x is not None: @@ -140,6 +295,28 @@ def f(x: str | Literal[1] | None): class D: if g != 1: reveal_type(g) # revealed: str + + if a.x is not None: + def _(): + if a.x != 1: + # TODO(#17643): should be `Unknown | str | None` + reveal_type(a.x) # revealed: Unknown | str | Literal[1] | None + + class D: + if a.x != 1: + # TODO(#17643): should be `Unknown | str` + reveal_type(a.x) # revealed: Unknown | str | Literal[1] | None + + if l[0] is not None: + def _(): + if l[0] != 1: + # TODO(#17643): should be `str | None` + reveal_type(l[0]) # revealed: str | Literal[1] | None + + class D: + if l[0] != 1: + # TODO(#17643): should be `str` + reveal_type(l[0]) # revealed: str | Literal[1] | None ``` ### Narrowing constraints with bindings in class scope, and nested scopes diff --git a/crates/ty_python_semantic/src/dunder_all.rs b/crates/ty_python_semantic/src/dunder_all.rs index 4265de2ac6d8f..6976e35e22443 100644 --- a/crates/ty_python_semantic/src/dunder_all.rs +++ b/crates/ty_python_semantic/src/dunder_all.rs @@ -7,7 +7,7 @@ use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt}; use ruff_python_ast::{self as ast}; use crate::semantic_index::ast_ids::HasScopedExpressionId; -use crate::semantic_index::symbol::ScopeId; +use crate::semantic_index::place::ScopeId; use crate::semantic_index::{SemanticIndex, global_scope, semantic_index}; use crate::types::{Truthiness, Type, infer_expression_types}; use crate::{Db, ModuleName, resolve_module}; diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index 1a204734c0f55..0123d28c171e9 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -24,13 +24,13 @@ pub(crate) mod list; mod module_name; mod module_resolver; mod node_key; +pub(crate) mod place; mod program; mod python_platform; pub mod semantic_index; mod semantic_model; pub(crate) mod site_packages; mod suppression; -pub(crate) mod symbol; pub mod types; mod unpack; mod util; diff --git a/crates/ty_python_semantic/src/symbol.rs b/crates/ty_python_semantic/src/place.rs similarity index 72% rename from crates/ty_python_semantic/src/symbol.rs rename to crates/ty_python_semantic/src/place.rs index d2b2518e61cc0..0f25a3ff30abd 100644 --- a/crates/ty_python_semantic/src/symbol.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -2,10 +2,10 @@ use ruff_db::files::File; use crate::dunder_all::dunder_all_names; use crate::module_resolver::file_to_module; -use crate::semantic_index::definition::Definition; -use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId}; +use crate::semantic_index::definition::{Definition, DefinitionState}; +use crate::semantic_index::place::{PlaceExpr, ScopeId, ScopedPlaceId}; use crate::semantic_index::{ - BindingWithConstraints, BindingWithConstraintsIterator, DeclarationsIterator, symbol_table, + BindingWithConstraints, BindingWithConstraintsIterator, DeclarationsIterator, place_table, }; use crate::semantic_index::{DeclarationWithConstraint, global_scope, use_def_map}; use crate::types::{ @@ -33,8 +33,8 @@ impl Boundness { } } -/// The result of a symbol lookup, which can either be a (possibly unbound) type -/// or a completely unbound symbol. +/// The result of a place lookup, which can either be a (possibly unbound) type +/// or a completely unbound place. /// /// Consider this example: /// ```py @@ -44,47 +44,47 @@ impl Boundness { /// possibly_unbound = 2 /// ``` /// -/// If we look up symbols in this scope, we would get the following results: +/// If we look up places in this scope, we would get the following results: /// ```rs -/// bound: Symbol::Type(Type::IntLiteral(1), Boundness::Bound), -/// possibly_unbound: Symbol::Type(Type::IntLiteral(2), Boundness::PossiblyUnbound), -/// non_existent: Symbol::Unbound, +/// bound: Place::Type(Type::IntLiteral(1), Boundness::Bound), +/// possibly_unbound: Place::Type(Type::IntLiteral(2), Boundness::PossiblyUnbound), +/// non_existent: Place::Unbound, /// ``` #[derive(Debug, Clone, PartialEq, Eq, salsa::Update)] -pub(crate) enum Symbol<'db> { +pub(crate) enum Place<'db> { Type(Type<'db>, Boundness), Unbound, } -impl<'db> Symbol<'db> { - /// Constructor that creates a `Symbol` with boundness [`Boundness::Bound`]. +impl<'db> Place<'db> { + /// Constructor that creates a `Place` with boundness [`Boundness::Bound`]. pub(crate) fn bound(ty: impl Into>) -> Self { - Symbol::Type(ty.into(), Boundness::Bound) + Place::Type(ty.into(), Boundness::Bound) } pub(crate) fn possibly_unbound(ty: impl Into>) -> Self { - Symbol::Type(ty.into(), Boundness::PossiblyUnbound) + Place::Type(ty.into(), Boundness::PossiblyUnbound) } - /// Constructor that creates a [`Symbol`] with a [`crate::types::TodoType`] type + /// Constructor that creates a [`Place`] with a [`crate::types::TodoType`] type /// and boundness [`Boundness::Bound`]. #[allow(unused_variables)] // Only unused in release builds pub(crate) fn todo(message: &'static str) -> Self { - Symbol::Type(todo_type!(message), Boundness::Bound) + Place::Type(todo_type!(message), Boundness::Bound) } pub(crate) fn is_unbound(&self) -> bool { - matches!(self, Symbol::Unbound) + matches!(self, Place::Unbound) } - /// Returns the type of the symbol, ignoring possible unboundness. + /// Returns the type of the place, ignoring possible unboundness. /// - /// If the symbol is *definitely* unbound, this function will return `None`. Otherwise, - /// if there is at least one control-flow path where the symbol is bound, return the type. + /// If the place is *definitely* unbound, this function will return `None`. Otherwise, + /// if there is at least one control-flow path where the place is bound, return the type. pub(crate) fn ignore_possibly_unbound(&self) -> Option> { match self { - Symbol::Type(ty, _) => Some(*ty), - Symbol::Unbound => None, + Place::Type(ty, _) => Some(*ty), + Place::Unbound => None, } } @@ -92,71 +92,71 @@ impl<'db> Symbol<'db> { #[track_caller] pub(crate) fn expect_type(self) -> Type<'db> { self.ignore_possibly_unbound() - .expect("Expected a (possibly unbound) type, not an unbound symbol") + .expect("Expected a (possibly unbound) type, not an unbound place") } #[must_use] - pub(crate) fn map_type(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Symbol<'db> { + pub(crate) fn map_type(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Place<'db> { match self { - Symbol::Type(ty, boundness) => Symbol::Type(f(ty), boundness), - Symbol::Unbound => Symbol::Unbound, + Place::Type(ty, boundness) => Place::Type(f(ty), boundness), + Place::Unbound => Place::Unbound, } } #[must_use] - pub(crate) fn with_qualifiers(self, qualifiers: TypeQualifiers) -> SymbolAndQualifiers<'db> { - SymbolAndQualifiers { - symbol: self, + pub(crate) fn with_qualifiers(self, qualifiers: TypeQualifiers) -> PlaceAndQualifiers<'db> { + PlaceAndQualifiers { + place: self, qualifiers, } } - /// Try to call `__get__(None, owner)` on the type of this symbol (not on the meta type). - /// If it succeeds, return the `__get__` return type. Otherwise, returns the original symbol. + /// Try to call `__get__(None, owner)` on the type of this place (not on the meta type). + /// If it succeeds, return the `__get__` return type. Otherwise, returns the original place. /// This is used to resolve (potential) descriptor attributes. - pub(crate) fn try_call_dunder_get(self, db: &'db dyn Db, owner: Type<'db>) -> Symbol<'db> { + pub(crate) fn try_call_dunder_get(self, db: &'db dyn Db, owner: Type<'db>) -> Place<'db> { match self { - Symbol::Type(Type::Union(union), boundness) => union.map_with_boundness(db, |elem| { - Symbol::Type(*elem, boundness).try_call_dunder_get(db, owner) + Place::Type(Type::Union(union), boundness) => union.map_with_boundness(db, |elem| { + Place::Type(*elem, boundness).try_call_dunder_get(db, owner) }), - Symbol::Type(Type::Intersection(intersection), boundness) => intersection + Place::Type(Type::Intersection(intersection), boundness) => intersection .map_with_boundness(db, |elem| { - Symbol::Type(*elem, boundness).try_call_dunder_get(db, owner) + Place::Type(*elem, boundness).try_call_dunder_get(db, owner) }), - Symbol::Type(self_ty, boundness) => { + Place::Type(self_ty, boundness) => { if let Some((dunder_get_return_ty, _)) = self_ty.try_call_dunder_get(db, Type::none(db), owner) { - Symbol::Type(dunder_get_return_ty, boundness) + Place::Type(dunder_get_return_ty, boundness) } else { self } } - Symbol::Unbound => Symbol::Unbound, + Place::Unbound => Place::Unbound, } } } -impl<'db> From> for SymbolAndQualifiers<'db> { +impl<'db> From> for PlaceAndQualifiers<'db> { fn from(value: LookupResult<'db>) -> Self { match value { Ok(type_and_qualifiers) => { - Symbol::Type(type_and_qualifiers.inner_type(), Boundness::Bound) + Place::Type(type_and_qualifiers.inner_type(), Boundness::Bound) .with_qualifiers(type_and_qualifiers.qualifiers()) } - Err(LookupError::Unbound(qualifiers)) => Symbol::Unbound.with_qualifiers(qualifiers), + Err(LookupError::Unbound(qualifiers)) => Place::Unbound.with_qualifiers(qualifiers), Err(LookupError::PossiblyUnbound(type_and_qualifiers)) => { - Symbol::Type(type_and_qualifiers.inner_type(), Boundness::PossiblyUnbound) + Place::Type(type_and_qualifiers.inner_type(), Boundness::PossiblyUnbound) .with_qualifiers(type_and_qualifiers.qualifiers()) } } } } -/// Possible ways in which a symbol lookup can (possibly or definitely) fail. +/// Possible ways in which a place lookup can (possibly or definitely) fail. #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub(crate) enum LookupError<'db> { Unbound(TypeQualifiers), @@ -168,7 +168,7 @@ impl<'db> LookupError<'db> { pub(crate) fn or_fall_back_to( self, db: &'db dyn Db, - fallback: SymbolAndQualifiers<'db>, + fallback: PlaceAndQualifiers<'db>, ) -> LookupResult<'db> { let fallback = fallback.into_lookup_result(); match (&self, &fallback) { @@ -188,34 +188,45 @@ impl<'db> LookupError<'db> { } } -/// A [`Result`] type in which the `Ok` variant represents a definitely bound symbol -/// and the `Err` variant represents a symbol that is either definitely or possibly unbound. +/// A [`Result`] type in which the `Ok` variant represents a definitely bound place +/// and the `Err` variant represents a place that is either definitely or possibly unbound. /// -/// Note that this type is exactly isomorphic to [`Symbol`]. -/// In the future, we could possibly consider removing `Symbol` and using this type everywhere instead. +/// Note that this type is exactly isomorphic to [`Place`]. +/// In the future, we could possibly consider removing `Place` and using this type everywhere instead. pub(crate) type LookupResult<'db> = Result, LookupError<'db>>; /// Infer the public type of a symbol (its type as seen from outside its scope) in the given /// `scope`. +#[allow(unused)] pub(crate) fn symbol<'db>( db: &'db dyn Db, scope: ScopeId<'db>, name: &str, -) -> SymbolAndQualifiers<'db> { +) -> PlaceAndQualifiers<'db> { symbol_impl(db, scope, name, RequiresExplicitReExport::No) } +/// Infer the public type of a place (its type as seen from outside its scope) in the given +/// `scope`. +pub(crate) fn place<'db>( + db: &'db dyn Db, + scope: ScopeId<'db>, + expr: &PlaceExpr, +) -> PlaceAndQualifiers<'db> { + place_impl(db, scope, expr, RequiresExplicitReExport::No) +} + /// Infer the public type of a class symbol (its type as seen from outside its scope) in the given /// `scope`. pub(crate) fn class_symbol<'db>( db: &'db dyn Db, scope: ScopeId<'db>, name: &str, -) -> SymbolAndQualifiers<'db> { - symbol_table(db, scope) - .symbol_id_by_name(name) +) -> PlaceAndQualifiers<'db> { + place_table(db, scope) + .place_id_by_name(name) .map(|symbol| { - let symbol_and_quals = symbol_by_id(db, scope, symbol, RequiresExplicitReExport::No); + let symbol_and_quals = place_by_id(db, scope, symbol, RequiresExplicitReExport::No); if symbol_and_quals.is_class_var() { // For declared class vars we do not need to check if they have bindings, @@ -223,27 +234,26 @@ pub(crate) fn class_symbol<'db>( return symbol_and_quals; } - if let SymbolAndQualifiers { - symbol: Symbol::Type(ty, _), + if let PlaceAndQualifiers { + place: Place::Type(ty, _), qualifiers, } = symbol_and_quals { // Otherwise, we need to check if the symbol has bindings let use_def = use_def_map(db, scope); let bindings = use_def.public_bindings(symbol); - let inferred = - symbol_from_bindings_impl(db, bindings, RequiresExplicitReExport::No); + let inferred = place_from_bindings_impl(db, bindings, RequiresExplicitReExport::No); // TODO: we should not need to calculate inferred type second time. This is a temporary // solution until the notion of Boundness and Declaredness is split. See #16036, #16264 match inferred { - Symbol::Unbound => Symbol::Unbound.with_qualifiers(qualifiers), - Symbol::Type(_, boundness) => { - Symbol::Type(ty, boundness).with_qualifiers(qualifiers) + Place::Unbound => Place::Unbound.with_qualifiers(qualifiers), + Place::Type(_, boundness) => { + Place::Type(ty, boundness).with_qualifiers(qualifiers) } } } else { - Symbol::Unbound.into() + Place::Unbound.into() } }) .unwrap_or_default() @@ -253,7 +263,7 @@ pub(crate) fn class_symbol<'db>( /// /// Note that all global scopes also include various "implicit globals" such as `__name__`, /// `__doc__` and `__file__`. This function **does not** consider those symbols; it will return -/// `Symbol::Unbound` for them. Use the (currently test-only) `global_symbol` query to also include +/// `Place::Unbound` for them. Use the (currently test-only) `global_symbol` query to also include /// those additional symbols. /// /// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports). @@ -261,7 +271,7 @@ pub(crate) fn explicit_global_symbol<'db>( db: &'db dyn Db, file: File, name: &str, -) -> SymbolAndQualifiers<'db> { +) -> PlaceAndQualifiers<'db> { symbol_impl( db, global_scope(db, file), @@ -277,11 +287,12 @@ pub(crate) fn explicit_global_symbol<'db>( /// rather than being looked up as symbols explicitly defined/declared in the global scope. /// /// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports). +#[allow(unused)] pub(crate) fn global_symbol<'db>( db: &'db dyn Db, file: File, name: &str, -) -> SymbolAndQualifiers<'db> { +) -> PlaceAndQualifiers<'db> { explicit_global_symbol(db, file, name) .or_fall_back_to(db, || module_type_implicit_global_symbol(db, name)) } @@ -295,7 +306,7 @@ pub(crate) fn imported_symbol<'db>( file: File, name: &str, requires_explicit_reexport: Option, -) -> SymbolAndQualifiers<'db> { +) -> PlaceAndQualifiers<'db> { let requires_explicit_reexport = requires_explicit_reexport.unwrap_or_else(|| { if file.is_stub(db.upcast()) { RequiresExplicitReExport::Yes @@ -323,9 +334,9 @@ pub(crate) fn imported_symbol<'db>( db, || { if name == "__getattr__" { - Symbol::Unbound.into() + Place::Unbound.into() } else if name == "__builtins__" { - Symbol::bound(Type::any()).into() + Place::bound(Type::any()).into() } else { KnownClass::ModuleType.to_instance(db).member(db, name) } @@ -335,12 +346,12 @@ pub(crate) fn imported_symbol<'db>( /// Lookup the type of `symbol` in the builtins namespace. /// -/// Returns `Symbol::Unbound` if the `builtins` module isn't available for some reason. +/// Returns `Place::Unbound` if the `builtins` module isn't available for some reason. /// /// Note that this function is only intended for use in the context of the builtins *namespace* /// and should not be used when a symbol is being explicitly imported from the `builtins` module /// (e.g. `from builtins import int`). -pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> SymbolAndQualifiers<'db> { +pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQualifiers<'db> { resolve_module(db, &KnownModule::Builtins.name()) .and_then(|module| { let file = module.file()?; @@ -364,12 +375,12 @@ pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> SymbolAndQu /// Lookup the type of `symbol` in a given known module. /// -/// Returns `Symbol::Unbound` if the given known module cannot be resolved for some reason. +/// Returns `Place::Unbound` if the given known module cannot be resolved for some reason. pub(crate) fn known_module_symbol<'db>( db: &'db dyn Db, known_module: KnownModule, symbol: &str, -) -> SymbolAndQualifiers<'db> { +) -> PlaceAndQualifiers<'db> { resolve_module(db, &known_module.name()) .and_then(|module| { let file = module.file()?; @@ -380,21 +391,21 @@ pub(crate) fn known_module_symbol<'db>( /// Lookup the type of `symbol` in the `typing` module namespace. /// -/// Returns `Symbol::Unbound` if the `typing` module isn't available for some reason. +/// Returns `Place::Unbound` if the `typing` module isn't available for some reason. #[inline] #[cfg(test)] -pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> SymbolAndQualifiers<'db> { +pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQualifiers<'db> { known_module_symbol(db, KnownModule::Typing, symbol) } /// Lookup the type of `symbol` in the `typing_extensions` module namespace. /// -/// Returns `Symbol::Unbound` if the `typing_extensions` module isn't available for some reason. +/// Returns `Place::Unbound` if the `typing_extensions` module isn't available for some reason. #[inline] pub(crate) fn typing_extensions_symbol<'db>( db: &'db dyn Db, symbol: &str, -) -> SymbolAndQualifiers<'db> { +) -> PlaceAndQualifiers<'db> { known_module_symbol(db, KnownModule::TypingExtensions, symbol) } @@ -414,14 +425,14 @@ fn core_module_scope(db: &dyn Db, core_module: KnownModule) -> Option( +pub(super) fn place_from_bindings<'db>( db: &'db dyn Db, bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>, -) -> Symbol<'db> { - symbol_from_bindings_impl(db, bindings_with_constraints, RequiresExplicitReExport::No) +) -> Place<'db> { + place_from_bindings_impl(db, bindings_with_constraints, RequiresExplicitReExport::No) } /// Build a declared type from a [`DeclarationsIterator`]. @@ -430,18 +441,18 @@ pub(super) fn symbol_from_bindings<'db>( /// `Ok(..)`. If there are conflicting declarations, returns an `Err(..)` variant with /// a union of the declared types as well as a list of all conflicting types. /// -/// This function also returns declaredness information (see [`Symbol`]) and a set of +/// This function also returns declaredness information (see [`Place`]) and a set of /// [`TypeQualifiers`] that have been specified on the declaration(s). -pub(crate) fn symbol_from_declarations<'db>( +pub(crate) fn place_from_declarations<'db>( db: &'db dyn Db, declarations: DeclarationsIterator<'_, 'db>, -) -> SymbolFromDeclarationsResult<'db> { - symbol_from_declarations_impl(db, declarations, RequiresExplicitReExport::No) +) -> PlaceFromDeclarationsResult<'db> { + place_from_declarations_impl(db, declarations, RequiresExplicitReExport::No) } -/// The result of looking up a declared type from declarations; see [`symbol_from_declarations`]. -pub(crate) type SymbolFromDeclarationsResult<'db> = - Result, (TypeAndQualifiers<'db>, Box<[Type<'db>]>)>; +/// The result of looking up a declared type from declarations; see [`place_from_declarations`]. +pub(crate) type PlaceFromDeclarationsResult<'db> = + Result, (TypeAndQualifiers<'db>, Box<[Type<'db>]>)>; /// A type with declaredness information, and a set of type qualifiers. /// @@ -458,33 +469,33 @@ pub(crate) type SymbolFromDeclarationsResult<'db> = /// /// [`CLASS_VAR`]: crate::types::TypeQualifiers::CLASS_VAR #[derive(Debug, Clone, PartialEq, Eq, salsa::Update)] -pub(crate) struct SymbolAndQualifiers<'db> { - pub(crate) symbol: Symbol<'db>, +pub(crate) struct PlaceAndQualifiers<'db> { + pub(crate) place: Place<'db>, pub(crate) qualifiers: TypeQualifiers, } -impl Default for SymbolAndQualifiers<'_> { +impl Default for PlaceAndQualifiers<'_> { fn default() -> Self { - SymbolAndQualifiers { - symbol: Symbol::Unbound, + PlaceAndQualifiers { + place: Place::Unbound, qualifiers: TypeQualifiers::empty(), } } } -impl<'db> SymbolAndQualifiers<'db> { - /// Constructor that creates a [`SymbolAndQualifiers`] instance with a [`TodoType`] type +impl<'db> PlaceAndQualifiers<'db> { + /// Constructor that creates a [`PlaceAndQualifiers`] instance with a [`TodoType`] type /// and no qualifiers. /// /// [`TodoType`]: crate::types::TodoType pub(crate) fn todo(message: &'static str) -> Self { Self { - symbol: Symbol::todo(message), + place: Place::todo(message), qualifiers: TypeQualifiers::empty(), } } - /// Returns `true` if the symbol has a `ClassVar` type qualifier. + /// Returns `true` if the place has a `ClassVar` type qualifier. pub(crate) fn is_class_var(&self) -> bool { self.qualifiers.contains(TypeQualifiers::CLASS_VAR) } @@ -493,41 +504,41 @@ impl<'db> SymbolAndQualifiers<'db> { pub(crate) fn map_type( self, f: impl FnOnce(Type<'db>) -> Type<'db>, - ) -> SymbolAndQualifiers<'db> { - SymbolAndQualifiers { - symbol: self.symbol.map_type(f), + ) -> PlaceAndQualifiers<'db> { + PlaceAndQualifiers { + place: self.place.map_type(f), qualifiers: self.qualifiers, } } - /// Transform symbol and qualifiers into a [`LookupResult`], - /// a [`Result`] type in which the `Ok` variant represents a definitely bound symbol - /// and the `Err` variant represents a symbol that is either definitely or possibly unbound. + /// Transform place and qualifiers into a [`LookupResult`], + /// a [`Result`] type in which the `Ok` variant represents a definitely bound place + /// and the `Err` variant represents a place that is either definitely or possibly unbound. pub(crate) fn into_lookup_result(self) -> LookupResult<'db> { match self { - SymbolAndQualifiers { - symbol: Symbol::Type(ty, Boundness::Bound), + PlaceAndQualifiers { + place: Place::Type(ty, Boundness::Bound), qualifiers, } => Ok(TypeAndQualifiers::new(ty, qualifiers)), - SymbolAndQualifiers { - symbol: Symbol::Type(ty, Boundness::PossiblyUnbound), + PlaceAndQualifiers { + place: Place::Type(ty, Boundness::PossiblyUnbound), qualifiers, } => Err(LookupError::PossiblyUnbound(TypeAndQualifiers::new( ty, qualifiers, ))), - SymbolAndQualifiers { - symbol: Symbol::Unbound, + PlaceAndQualifiers { + place: Place::Unbound, qualifiers, } => Err(LookupError::Unbound(qualifiers)), } } - /// Safely unwrap the symbol and the qualifiers into a [`TypeQualifiers`]. + /// Safely unwrap the place and the qualifiers into a [`TypeQualifiers`]. /// - /// If the symbol is definitely unbound or possibly unbound, it will be transformed into a + /// If the place is definitely unbound or possibly unbound, it will be transformed into a /// [`LookupError`] and `diagnostic_fn` will be applied to the error value before returning /// the result of `diagnostic_fn` (which will be a [`TypeQualifiers`]). This allows the caller - /// to ensure that a diagnostic is emitted if the symbol is possibly or definitely unbound. + /// to ensure that a diagnostic is emitted if the place is possibly or definitely unbound. pub(crate) fn unwrap_with_diagnostic( self, diagnostic_fn: impl FnOnce(LookupError<'db>) -> TypeAndQualifiers<'db>, @@ -535,21 +546,21 @@ impl<'db> SymbolAndQualifiers<'db> { self.into_lookup_result().unwrap_or_else(diagnostic_fn) } - /// Fallback (partially or fully) to another symbol if `self` is partially or fully unbound. + /// Fallback (partially or fully) to another place if `self` is partially or fully unbound. /// /// 1. If `self` is definitely bound, return `self` without evaluating `fallback_fn()`. /// 2. Else, evaluate `fallback_fn()`: /// 1. If `self` is definitely unbound, return the result of `fallback_fn()`. /// 2. Else, if `fallback` is definitely unbound, return `self`. /// 3. Else, if `self` is possibly unbound and `fallback` is definitely bound, - /// return `Symbol(, Boundness::Bound)` + /// return `Place(, Boundness::Bound)` /// 4. Else, if `self` is possibly unbound and `fallback` is possibly unbound, - /// return `Symbol(, Boundness::PossiblyUnbound)` + /// return `Place(, Boundness::PossiblyUnbound)` #[must_use] pub(crate) fn or_fall_back_to( self, db: &'db dyn Db, - fallback_fn: impl FnOnce() -> SymbolAndQualifiers<'db>, + fallback_fn: impl FnOnce() -> PlaceAndQualifiers<'db>, ) -> Self { self.into_lookup_result() .or_else(|lookup_error| lookup_error.or_fall_back_to(db, fallback_fn())) @@ -557,87 +568,87 @@ impl<'db> SymbolAndQualifiers<'db> { } } -impl<'db> From> for SymbolAndQualifiers<'db> { - fn from(symbol: Symbol<'db>) -> Self { - symbol.with_qualifiers(TypeQualifiers::empty()) +impl<'db> From> for PlaceAndQualifiers<'db> { + fn from(place: Place<'db>) -> Self { + place.with_qualifiers(TypeQualifiers::empty()) } } -fn symbol_cycle_recover<'db>( +fn place_cycle_recover<'db>( _db: &'db dyn Db, - _value: &SymbolAndQualifiers<'db>, + _value: &PlaceAndQualifiers<'db>, _count: u32, _scope: ScopeId<'db>, - _symbol_id: ScopedSymbolId, + _place_id: ScopedPlaceId, _requires_explicit_reexport: RequiresExplicitReExport, -) -> salsa::CycleRecoveryAction> { +) -> salsa::CycleRecoveryAction> { salsa::CycleRecoveryAction::Iterate } -fn symbol_cycle_initial<'db>( +fn place_cycle_initial<'db>( _db: &'db dyn Db, _scope: ScopeId<'db>, - _symbol_id: ScopedSymbolId, + _place_id: ScopedPlaceId, _requires_explicit_reexport: RequiresExplicitReExport, -) -> SymbolAndQualifiers<'db> { - Symbol::bound(Type::Never).into() +) -> PlaceAndQualifiers<'db> { + Place::bound(Type::Never).into() } -#[salsa::tracked(cycle_fn=symbol_cycle_recover, cycle_initial=symbol_cycle_initial)] -fn symbol_by_id<'db>( +#[salsa::tracked(cycle_fn=place_cycle_recover, cycle_initial=place_cycle_initial)] +fn place_by_id<'db>( db: &'db dyn Db, scope: ScopeId<'db>, - symbol_id: ScopedSymbolId, + place_id: ScopedPlaceId, requires_explicit_reexport: RequiresExplicitReExport, -) -> SymbolAndQualifiers<'db> { +) -> PlaceAndQualifiers<'db> { let use_def = use_def_map(db, scope); - // If the symbol is declared, the public type is based on declarations; otherwise, it's based + // If the place is declared, the public type is based on declarations; otherwise, it's based // on inference from bindings. - let declarations = use_def.public_declarations(symbol_id); - let declared = symbol_from_declarations_impl(db, declarations, requires_explicit_reexport); + let declarations = use_def.public_declarations(place_id); + let declared = place_from_declarations_impl(db, declarations, requires_explicit_reexport); match declared { - // Symbol is declared, trust the declared type + // Place is declared, trust the declared type Ok( - symbol_and_quals @ SymbolAndQualifiers { - symbol: Symbol::Type(_, Boundness::Bound), + place_and_quals @ PlaceAndQualifiers { + place: Place::Type(_, Boundness::Bound), qualifiers: _, }, - ) => symbol_and_quals, - // Symbol is possibly declared - Ok(SymbolAndQualifiers { - symbol: Symbol::Type(declared_ty, Boundness::PossiblyUnbound), + ) => place_and_quals, + // Place is possibly declared + Ok(PlaceAndQualifiers { + place: Place::Type(declared_ty, Boundness::PossiblyUnbound), qualifiers, }) => { - let bindings = use_def.public_bindings(symbol_id); - let inferred = symbol_from_bindings_impl(db, bindings, requires_explicit_reexport); + let bindings = use_def.public_bindings(place_id); + let inferred = place_from_bindings_impl(db, bindings, requires_explicit_reexport); - let symbol = match inferred { - // Symbol is possibly undeclared and definitely unbound - Symbol::Unbound => { + let place = match inferred { + // Place is possibly undeclared and definitely unbound + Place::Unbound => { // TODO: We probably don't want to report `Bound` here. This requires a bit of // design work though as we might want a different behavior for stubs and for // normal modules. - Symbol::Type(declared_ty, Boundness::Bound) + Place::Type(declared_ty, Boundness::Bound) } - // Symbol is possibly undeclared and (possibly) bound - Symbol::Type(inferred_ty, boundness) => Symbol::Type( + // Place is possibly undeclared and (possibly) bound + Place::Type(inferred_ty, boundness) => Place::Type( UnionType::from_elements(db, [inferred_ty, declared_ty]), boundness, ), }; - SymbolAndQualifiers { symbol, qualifiers } + PlaceAndQualifiers { place, qualifiers } } - // Symbol is undeclared, return the union of `Unknown` with the inferred type - Ok(SymbolAndQualifiers { - symbol: Symbol::Unbound, + // Place is undeclared, return the union of `Unknown` with the inferred type + Ok(PlaceAndQualifiers { + place: Place::Unbound, qualifiers: _, }) => { - let bindings = use_def.public_bindings(symbol_id); - let inferred = symbol_from_bindings_impl(db, bindings, requires_explicit_reexport); + let bindings = use_def.public_bindings(place_id); + let inferred = place_from_bindings_impl(db, bindings, requires_explicit_reexport); // `__slots__` is a symbol with special behavior in Python's runtime. It can be // modified externally, but those changes do not take effect. We therefore issue @@ -648,13 +659,12 @@ fn symbol_by_id<'db>( // `TYPE_CHECKING` is a special variable that should only be assigned `False` // at runtime, but is always considered `True` in type checking. // See mdtest/known_constants.md#user-defined-type_checking for details. - let is_considered_non_modifiable = matches!( - symbol_table(db, scope).symbol(symbol_id).name().as_str(), - "__slots__" | "TYPE_CHECKING" - ); + let is_considered_non_modifiable = place_table(db, scope) + .place_expr(place_id) + .is_name_and(|name| matches!(name, "__slots__" | "TYPE_CHECKING")); if scope.file(db).is_stub(db.upcast()) { - // We generally trust module-level undeclared symbols in stubs and do not union + // We generally trust module-level undeclared places in stubs and do not union // with `Unknown`. If we don't do this, simple aliases like `IOError = OSError` in // stubs would result in `IOError` being a union of `OSError` and `Unknown`, which // leads to all sorts of downstream problems. Similarly, type variables are often @@ -666,17 +676,17 @@ fn symbol_by_id<'db>( .into() } } - // Symbol has conflicting declared types + // Place has conflicting declared types Err((declared, _)) => { // Intentionally ignore conflicting declared types; that's not our problem, // it's the problem of the module we are importing from. - Symbol::bound(declared.inner_type()).with_qualifiers(declared.qualifiers()) + Place::bound(declared.inner_type()).with_qualifiers(declared.qualifiers()) } } // TODO (ticket: https://github.com/astral-sh/ruff/issues/14297) Our handling of boundness // currently only depends on bindings, and ignores declarations. This is inconsistent, since - // we only look at bindings if the symbol may be undeclared. Consider the following example: + // we only look at bindings if the place may be undeclared. Consider the following example: // ```py // x: int // @@ -685,7 +695,7 @@ fn symbol_by_id<'db>( // else // y = 3 // ``` - // If we import from this module, we will currently report `x` as a definitely-bound symbol + // If we import from this module, we will currently report `x` as a definitely-bound place // (even though it has no bindings at all!) but report `y` as possibly-unbound (even though // every path has either a binding or a declaration for it.) } @@ -696,7 +706,7 @@ fn symbol_impl<'db>( scope: ScopeId<'db>, name: &str, requires_explicit_reexport: RequiresExplicitReExport, -) -> SymbolAndQualifiers<'db> { +) -> PlaceAndQualifiers<'db> { let _span = tracing::trace_span!("symbol", ?name).entered(); if name == "platform" @@ -705,7 +715,7 @@ fn symbol_impl<'db>( { match Program::get(db).python_platform(db) { crate::PythonPlatform::Identifier(platform) => { - return Symbol::bound(Type::string_literal(db, platform.as_str())).into(); + return Place::bound(Type::string_literal(db, platform.as_str())).into(); } crate::PythonPlatform::All => { // Fall through to the looked up type @@ -713,22 +723,37 @@ fn symbol_impl<'db>( } } - symbol_table(db, scope) - .symbol_id_by_name(name) - .map(|symbol| symbol_by_id(db, scope, symbol, requires_explicit_reexport)) + place_table(db, scope) + .place_id_by_name(name) + .map(|symbol| place_by_id(db, scope, symbol, requires_explicit_reexport)) + .unwrap_or_default() +} + +/// Implementation of [`place`]. +fn place_impl<'db>( + db: &'db dyn Db, + scope: ScopeId<'db>, + expr: &PlaceExpr, + requires_explicit_reexport: RequiresExplicitReExport, +) -> PlaceAndQualifiers<'db> { + let _span = tracing::trace_span!("place", ?expr).entered(); + + place_table(db, scope) + .place_id_by_expr(expr) + .map(|place| place_by_id(db, scope, place, requires_explicit_reexport)) .unwrap_or_default() } -/// Implementation of [`symbol_from_bindings`]. +/// Implementation of [`place_from_bindings`]. /// /// ## Implementation Note /// This function gets called cross-module. It, therefore, shouldn't /// access any AST nodes from the file containing the declarations. -fn symbol_from_bindings_impl<'db>( +fn place_from_bindings_impl<'db>( db: &'db dyn Db, bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>, requires_explicit_reexport: RequiresExplicitReExport, -) -> Symbol<'db> { +) -> Place<'db> { let predicates = bindings_with_constraints.predicates; let visibility_constraints = bindings_with_constraints.visibility_constraints; let mut bindings_with_constraints = bindings_with_constraints.peekable(); @@ -742,9 +767,10 @@ fn symbol_from_bindings_impl<'db>( binding, visibility_constraint, narrowing_constraint: _, - }) if binding.is_none_or(is_non_exported) => Some(*visibility_constraint), + }) if binding.is_undefined_or(is_non_exported) => Some(*visibility_constraint), _ => None, }; + let mut deleted_visibility = Truthiness::AlwaysFalse; // Evaluate this lazily because we don't always need it (for example, if there are no visible // bindings at all, we don't need it), and it can cause us to evaluate visibility constraint @@ -763,7 +789,19 @@ fn symbol_from_bindings_impl<'db>( narrowing_constraint, visibility_constraint, }| { - let binding = binding?; + let binding = + match binding { + DefinitionState::Defined(binding) => binding, + DefinitionState::Undefined => { + return None; + } + DefinitionState::Deleted => { + deleted_visibility = deleted_visibility.or( + visibility_constraints.evaluate(db, predicates, visibility_constraint) + ); + return None; + } + }; if is_non_exported(binding) { return None; @@ -774,7 +812,7 @@ fn symbol_from_bindings_impl<'db>( if static_visibility.is_always_false() { // We found a binding that we have statically determined to not be visible from - // the use of the symbol that we are investigating. There are three interesting + // the use of the place that we are investigating. There are three interesting // cases to consider: // // ```py @@ -824,7 +862,7 @@ fn symbol_from_bindings_impl<'db>( } let binding_ty = binding_type(db, binding); - Some(narrowing_constraint.narrow(db, binding_ty, binding.symbol(db))) + Some(narrowing_constraint.narrow(db, binding_ty, binding.place(db))) }, ); @@ -839,29 +877,31 @@ fn symbol_from_bindings_impl<'db>( Truthiness::Ambiguous => Boundness::PossiblyUnbound, }; - if let Some(second) = types.next() { - Symbol::Type( - UnionType::from_elements(db, [first, second].into_iter().chain(types)), - boundness, - ) + let ty = if let Some(second) = types.next() { + UnionType::from_elements(db, [first, second].into_iter().chain(types)) } else { - Symbol::Type(first, boundness) + first + }; + match deleted_visibility { + Truthiness::AlwaysFalse => Place::Type(ty, boundness), + Truthiness::AlwaysTrue => Place::Unbound, + Truthiness::Ambiguous => Place::Type(ty, Boundness::PossiblyUnbound), } } else { - Symbol::Unbound + Place::Unbound } } -/// Implementation of [`symbol_from_declarations`]. +/// Implementation of [`place_from_declarations`]. /// /// ## Implementation Note /// This function gets called cross-module. It, therefore, shouldn't /// access any AST nodes from the file containing the declarations. -fn symbol_from_declarations_impl<'db>( +fn place_from_declarations_impl<'db>( db: &'db dyn Db, declarations: DeclarationsIterator<'_, 'db>, requires_explicit_reexport: RequiresExplicitReExport, -) -> SymbolFromDeclarationsResult<'db> { +) -> PlaceFromDeclarationsResult<'db> { let predicates = declarations.predicates; let visibility_constraints = declarations.visibility_constraints; let mut declarations = declarations.peekable(); @@ -874,7 +914,7 @@ fn symbol_from_declarations_impl<'db>( Some(DeclarationWithConstraint { declaration, visibility_constraint, - }) if declaration.is_none_or(is_non_exported) => { + }) if declaration.is_undefined_or(is_non_exported) => { visibility_constraints.evaluate(db, predicates, *visibility_constraint) } _ => Truthiness::AlwaysFalse, @@ -885,7 +925,9 @@ fn symbol_from_declarations_impl<'db>( declaration, visibility_constraint, }| { - let declaration = declaration?; + let DefinitionState::Defined(declaration) = declaration else { + return None; + }; if is_non_exported(declaration) { return None; @@ -932,8 +974,10 @@ fn symbol_from_declarations_impl<'db>( Truthiness::Ambiguous => Boundness::PossiblyUnbound, }; - Ok(Symbol::Type(declared.inner_type(), boundness) - .with_qualifiers(declared.qualifiers())) + Ok( + Place::Type(declared.inner_type(), boundness) + .with_qualifiers(declared.qualifiers()), + ) } else { Err(( declared, @@ -943,7 +987,7 @@ fn symbol_from_declarations_impl<'db>( )) } } else { - Ok(Symbol::Unbound.into()) + Ok(Place::Unbound.into()) } } @@ -963,8 +1007,8 @@ fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool { let Some(all_names) = dunder_all_names(db, definition.file(db)) else { return false; }; - let table = symbol_table(db, definition.scope(db)); - let symbol_name = table.symbol(definition.symbol(db)).name(); + let table = place_table(db, definition.scope(db)); + let symbol_name = table.place_expr(definition.place(db)).expect_name(); all_names.contains(symbol_name) } @@ -972,38 +1016,39 @@ mod implicit_globals { use ruff_python_ast as ast; use crate::db::Db; - use crate::semantic_index::{self, symbol_table, use_def_map}; - use crate::symbol::SymbolAndQualifiers; + use crate::place::PlaceAndQualifiers; + use crate::semantic_index::place::PlaceExpr; + use crate::semantic_index::{self, place_table, use_def_map}; use crate::types::{KnownClass, Type}; - use super::{Symbol, SymbolFromDeclarationsResult, symbol_from_declarations}; + use super::{Place, PlaceFromDeclarationsResult, place_from_declarations}; pub(crate) fn module_type_implicit_global_declaration<'db>( db: &'db dyn Db, - name: &str, - ) -> SymbolFromDeclarationsResult<'db> { + expr: &PlaceExpr, + ) -> PlaceFromDeclarationsResult<'db> { if !module_type_symbols(db) .iter() - .any(|module_type_member| &**module_type_member == name) + .any(|module_type_member| Some(module_type_member) == expr.as_name()) { - return Ok(Symbol::Unbound.into()); + return Ok(Place::Unbound.into()); } let Type::ClassLiteral(module_type_class) = KnownClass::ModuleType.to_class_literal(db) else { - return Ok(Symbol::Unbound.into()); + return Ok(Place::Unbound.into()); }; let module_type_scope = module_type_class.body_scope(db); - let symbol_table = symbol_table(db, module_type_scope); - let Some(symbol_id) = symbol_table.symbol_id_by_name(name) else { - return Ok(Symbol::Unbound.into()); + let place_table = place_table(db, module_type_scope); + let Some(place_id) = place_table.place_id_by_expr(expr) else { + return Ok(Place::Unbound.into()); }; - symbol_from_declarations( + place_from_declarations( db, - use_def_map(db, module_type_scope).public_declarations(symbol_id), + use_def_map(db, module_type_scope).public_declarations(place_id), ) } - /// Looks up the type of an "implicit global symbol". Returns [`Symbol::Unbound`] if + /// Looks up the type of an "implicit global symbol". Returns [`Place::Unbound`] if /// `name` is not present as an implicit symbol in module-global namespaces. /// /// Implicit global symbols are symbols such as `__doc__`, `__name__`, and `__file__` @@ -1014,20 +1059,20 @@ mod implicit_globals { /// up in the global scope **from within the same file**. If the symbol is being looked up /// from outside the file (e.g. via imports), use [`super::imported_symbol`] (or fallback logic /// like the logic used in that function) instead. The reason is that this function returns - /// [`Symbol::Unbound`] for `__init__` and `__dict__` (which cannot be found in globals if + /// [`Place::Unbound`] for `__init__` and `__dict__` (which cannot be found in globals if /// the lookup is being done from the same file) -- but these symbols *are* available in the /// global scope if they're being imported **from a different file**. pub(crate) fn module_type_implicit_global_symbol<'db>( db: &'db dyn Db, name: &str, - ) -> SymbolAndQualifiers<'db> { + ) -> PlaceAndQualifiers<'db> { // We special-case `__file__` here because we know that for an internal implicit global // lookup in a Python module, it is always a string, even though typeshed says `str | // None`. if name == "__file__" { - Symbol::bound(KnownClass::Str.to_instance(db)).into() + Place::bound(KnownClass::Str.to_instance(db)).into() } else if name == "__builtins__" { - Symbol::bound(Type::any()).into() + Place::bound(Type::any()).into() } // In general we wouldn't check to see whether a symbol exists on a class before doing the // `.member()` call on the instance type -- we'd just do the `.member`() call on the instance @@ -1040,7 +1085,7 @@ mod implicit_globals { { KnownClass::ModuleType.to_instance(db).member(db, name) } else { - Symbol::Unbound.into() + Place::Unbound.into() } } @@ -1073,12 +1118,12 @@ mod implicit_globals { }; let module_type_scope = module_type.body_scope(db); - let module_type_symbol_table = symbol_table(db, module_type_scope); + let module_type_symbol_table = place_table(db, module_type_scope); module_type_symbol_table - .symbols() - .filter(|symbol| symbol.is_declared()) - .map(semantic_index::symbol::Symbol::name) + .places() + .filter(|symbol| symbol.is_declared() && symbol.is_name()) + .map(semantic_index::place::PlaceExpr::expect_name) .filter(|symbol_name| { !matches!(&***symbol_name, "__dict__" | "__getattr__" | "__init__") }) @@ -1123,9 +1168,9 @@ impl RequiresExplicitReExport { /// of symbols that have no declared type. fn widen_type_for_undeclared_public_symbol<'db>( db: &'db dyn Db, - inferred: Symbol<'db>, + inferred: Place<'db>, is_considered_non_modifiable: bool, -) -> Symbol<'db> { +) -> Place<'db> { // We special-case known-instance types here since symbols like `typing.Any` are typically // not declared in the stubs (e.g. `Any = object()`), but we still want to treat them as // such. @@ -1153,15 +1198,15 @@ mod tests { let ty1 = Type::IntLiteral(1); let ty2 = Type::IntLiteral(2); - let unbound = || Symbol::Unbound.with_qualifiers(TypeQualifiers::empty()); + let unbound = || Place::Unbound.with_qualifiers(TypeQualifiers::empty()); let possibly_unbound_ty1 = - || Symbol::Type(ty1, PossiblyUnbound).with_qualifiers(TypeQualifiers::empty()); + || Place::Type(ty1, PossiblyUnbound).with_qualifiers(TypeQualifiers::empty()); let possibly_unbound_ty2 = - || Symbol::Type(ty2, PossiblyUnbound).with_qualifiers(TypeQualifiers::empty()); + || Place::Type(ty2, PossiblyUnbound).with_qualifiers(TypeQualifiers::empty()); - let bound_ty1 = || Symbol::Type(ty1, Bound).with_qualifiers(TypeQualifiers::empty()); - let bound_ty2 = || Symbol::Type(ty2, Bound).with_qualifiers(TypeQualifiers::empty()); + let bound_ty1 = || Place::Type(ty1, Bound).with_qualifiers(TypeQualifiers::empty()); + let bound_ty2 = || Place::Type(ty2, Bound).with_qualifiers(TypeQualifiers::empty()); // Start from an unbound symbol assert_eq!(unbound().or_fall_back_to(&db, unbound), unbound()); @@ -1178,11 +1223,11 @@ mod tests { ); assert_eq!( possibly_unbound_ty1().or_fall_back_to(&db, possibly_unbound_ty2), - Symbol::Type(UnionType::from_elements(&db, [ty1, ty2]), PossiblyUnbound).into() + Place::Type(UnionType::from_elements(&db, [ty1, ty2]), PossiblyUnbound).into() ); assert_eq!( possibly_unbound_ty1().or_fall_back_to(&db, bound_ty2), - Symbol::Type(UnionType::from_elements(&db, [ty1, ty2]), Bound).into() + Place::Type(UnionType::from_elements(&db, [ty1, ty2]), Bound).into() ); // Start from a definitely bound symbol @@ -1195,10 +1240,10 @@ mod tests { } #[track_caller] - fn assert_bound_string_symbol<'db>(db: &'db dyn Db, symbol: Symbol<'db>) { + fn assert_bound_string_symbol<'db>(db: &'db dyn Db, symbol: Place<'db>) { assert!(matches!( symbol, - Symbol::Type(Type::NominalInstance(_), Boundness::Bound) + Place::Type(Type::NominalInstance(_), Boundness::Bound) )); assert_eq!(symbol.expect_type(), KnownClass::Str.to_instance(db)); } @@ -1206,19 +1251,19 @@ mod tests { #[test] fn implicit_builtin_globals() { let db = setup_db(); - assert_bound_string_symbol(&db, builtins_symbol(&db, "__name__").symbol); + assert_bound_string_symbol(&db, builtins_symbol(&db, "__name__").place); } #[test] fn implicit_typing_globals() { let db = setup_db(); - assert_bound_string_symbol(&db, typing_symbol(&db, "__name__").symbol); + assert_bound_string_symbol(&db, typing_symbol(&db, "__name__").place); } #[test] fn implicit_typing_extensions_globals() { let db = setup_db(); - assert_bound_string_symbol(&db, typing_extensions_symbol(&db, "__name__").symbol); + assert_bound_string_symbol(&db, typing_extensions_symbol(&db, "__name__").place); } #[test] @@ -1226,7 +1271,7 @@ mod tests { let db = setup_db(); assert_bound_string_symbol( &db, - known_module_symbol(&db, KnownModule::Sys, "__name__").symbol, + known_module_symbol(&db, KnownModule::Sys, "__name__").place, ); } } diff --git a/crates/ty_python_semantic/src/semantic_index.rs b/crates/ty_python_semantic/src/semantic_index.rs index c3a2f19418eba..c117b7f737435 100644 --- a/crates/ty_python_semantic/src/semantic_index.rs +++ b/crates/ty_python_semantic/src/semantic_index.rs @@ -19,9 +19,9 @@ use crate::semantic_index::builder::SemanticIndexBuilder; use crate::semantic_index::definition::{Definition, DefinitionNodeKey, Definitions}; use crate::semantic_index::expression::Expression; use crate::semantic_index::narrowing_constraints::ScopedNarrowingConstraint; -use crate::semantic_index::symbol::{ - FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopeKind, ScopedSymbolId, - SymbolTable, +use crate::semantic_index::place::{ + FileScopeId, NodeWithScopeKey, NodeWithScopeRef, PlaceExpr, PlaceTable, Scope, ScopeId, + ScopeKind, ScopedPlaceId, }; use crate::semantic_index::use_def::{EagerSnapshotKey, ScopedEagerSnapshotId, UseDefMap}; @@ -30,9 +30,9 @@ mod builder; pub mod definition; pub mod expression; pub(crate) mod narrowing_constraints; +pub mod place; pub(crate) mod predicate; mod re_exports; -pub mod symbol; mod use_def; mod visibility_constraints; @@ -41,7 +41,7 @@ pub(crate) use self::use_def::{ DeclarationsIterator, }; -type SymbolMap = hashbrown::HashMap; +type PlaceSet = hashbrown::HashMap; /// Returns the semantic index for `file`. /// @@ -55,18 +55,18 @@ pub(crate) fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> { SemanticIndexBuilder::new(db, file, parsed).build() } -/// Returns the symbol table for a specific `scope`. +/// Returns the place table for a specific `scope`. /// -/// Using [`symbol_table`] over [`semantic_index`] has the advantage that -/// Salsa can avoid invalidating dependent queries if this scope's symbol table +/// Using [`place_table`] over [`semantic_index`] has the advantage that +/// Salsa can avoid invalidating dependent queries if this scope's place table /// is unchanged. #[salsa::tracked(returns(deref))] -pub(crate) fn symbol_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc { +pub(crate) fn place_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc { let file = scope.file(db); - let _span = tracing::trace_span!("symbol_table", scope=?scope.as_id(), ?file).entered(); + let _span = tracing::trace_span!("place_table", scope=?scope.as_id(), ?file).entered(); let index = semantic_index(db, file); - index.symbol_table(scope.file_scope_id(db)) + index.place_table(scope.file_scope_id(db)) } /// Returns the set of modules that are imported anywhere in `file`. @@ -113,13 +113,10 @@ pub(crate) fn attribute_assignments<'db, 's>( let index = semantic_index(db, file); attribute_scopes(db, class_body_scope).filter_map(|function_scope_id| { - let attribute_table = index.instance_attribute_table(function_scope_id); - let symbol = attribute_table.symbol_id_by_name(name)?; + let place_table = index.place_table(function_scope_id); + let place = place_table.place_id_by_instance_attribute_name(name)?; let use_def = &index.use_def_maps[function_scope_id]; - Some(( - use_def.instance_attribute_bindings(symbol), - function_scope_id, - )) + Some((use_def.public_bindings(place), function_scope_id)) }) } @@ -167,14 +164,11 @@ pub(crate) enum EagerSnapshotResult<'map, 'db> { NoLongerInEagerContext, } -/// The symbol tables and use-def maps for all scopes in a file. +/// The place tables and use-def maps for all scopes in a file. #[derive(Debug, Update)] pub(crate) struct SemanticIndex<'db> { - /// List of all symbol tables in this file, indexed by scope. - symbol_tables: IndexVec>, - - /// List of all instance attribute tables in this file, indexed by scope. - instance_attribute_tables: IndexVec, + /// List of all place tables in this file, indexed by scope. + place_tables: IndexVec>, /// List of all scopes in this file. scopes: IndexVec, @@ -195,7 +189,7 @@ pub(crate) struct SemanticIndex<'db> { scope_ids_by_scope: IndexVec>, /// Map from the file-local [`FileScopeId`] to the set of explicit-global symbols it contains. - globals_by_scope: FxHashMap>, + globals_by_scope: FxHashMap>, /// Use-def map for each scope in this file. use_def_maps: IndexVec>>, @@ -223,17 +217,13 @@ pub(crate) struct SemanticIndex<'db> { } impl<'db> SemanticIndex<'db> { - /// Returns the symbol table for a specific scope. + /// Returns the place table for a specific scope. /// - /// Use the Salsa cached [`symbol_table()`] query if you only need the - /// symbol table for a single scope. + /// Use the Salsa cached [`place_table()`] query if you only need the + /// place table for a single scope. #[track_caller] - pub(super) fn symbol_table(&self, scope_id: FileScopeId) -> Arc { - self.symbol_tables[scope_id].clone() - } - - pub(super) fn instance_attribute_table(&self, scope_id: FileScopeId) -> &SymbolTable { - &self.instance_attribute_tables[scope_id] + pub(super) fn place_table(&self, scope_id: FileScopeId) -> Arc { + self.place_tables[scope_id].clone() } /// Returns the use-def map for a specific scope. @@ -286,7 +276,7 @@ impl<'db> SemanticIndex<'db> { pub(crate) fn symbol_is_global_in_scope( &self, - symbol: ScopedSymbolId, + symbol: ScopedPlaceId, scope: FileScopeId, ) -> bool { self.globals_by_scope @@ -444,7 +434,7 @@ impl<'db> SemanticIndex<'db> { pub(crate) fn eager_snapshot( &self, enclosing_scope: FileScopeId, - symbol: &str, + expr: &PlaceExpr, nested_scope: FileScopeId, ) -> EagerSnapshotResult<'_, 'db> { for (ancestor_scope_id, ancestor_scope) in self.ancestor_scopes(nested_scope) { @@ -455,12 +445,12 @@ impl<'db> SemanticIndex<'db> { return EagerSnapshotResult::NoLongerInEagerContext; } } - let Some(symbol_id) = self.symbol_tables[enclosing_scope].symbol_id_by_name(symbol) else { + let Some(place_id) = self.place_tables[enclosing_scope].place_id_by_expr(expr) else { return EagerSnapshotResult::NotFound; }; let key = EagerSnapshotKey { enclosing_scope, - enclosing_symbol: symbol_id, + enclosing_place: place_id, nested_scope, }; let Some(id) = self.eager_snapshots.get(&key) else { @@ -480,9 +470,9 @@ pub struct AncestorsIter<'a> { } impl<'a> AncestorsIter<'a> { - fn new(module_symbol_table: &'a SemanticIndex, start: FileScopeId) -> Self { + fn new(module_table: &'a SemanticIndex, start: FileScopeId) -> Self { Self { - scopes: &module_symbol_table.scopes, + scopes: &module_table.scopes, next_id: Some(start), } } @@ -508,9 +498,9 @@ pub struct DescendantsIter<'a> { } impl<'a> DescendantsIter<'a> { - fn new(symbol_table: &'a SemanticIndex, scope_id: FileScopeId) -> Self { - let scope = &symbol_table.scopes[scope_id]; - let scopes = &symbol_table.scopes[scope.descendants()]; + fn new(index: &'a SemanticIndex, scope_id: FileScopeId) -> Self { + let scope = &index.scopes[scope_id]; + let scopes = &index.scopes[scope.descendants()]; Self { next_id: scope_id + 1, @@ -545,8 +535,8 @@ pub struct ChildrenIter<'a> { } impl<'a> ChildrenIter<'a> { - pub(crate) fn new(module_symbol_table: &'a SemanticIndex, parent: FileScopeId) -> Self { - let descendants = DescendantsIter::new(module_symbol_table, parent); + pub(crate) fn new(module_index: &'a SemanticIndex, parent: FileScopeId) -> Self { + let descendants = DescendantsIter::new(module_index, parent); Self { parent, @@ -577,21 +567,19 @@ mod tests { use crate::db::tests::{TestDb, TestDbBuilder}; use crate::semantic_index::ast_ids::{HasScopedUseId, ScopedUseId}; use crate::semantic_index::definition::{Definition, DefinitionKind}; - use crate::semantic_index::symbol::{ - FileScopeId, Scope, ScopeKind, ScopedSymbolId, SymbolTable, - }; + use crate::semantic_index::place::{FileScopeId, PlaceTable, Scope, ScopeKind, ScopedPlaceId}; use crate::semantic_index::use_def::UseDefMap; - use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map}; + use crate::semantic_index::{global_scope, place_table, semantic_index, use_def_map}; impl UseDefMap<'_> { - fn first_public_binding(&self, symbol: ScopedSymbolId) -> Option> { + fn first_public_binding(&self, symbol: ScopedPlaceId) -> Option> { self.public_bindings(symbol) - .find_map(|constrained_binding| constrained_binding.binding) + .find_map(|constrained_binding| constrained_binding.binding.definition()) } fn first_binding_at_use(&self, use_id: ScopedUseId) -> Option> { self.bindings_at_use(use_id) - .find_map(|constrained_binding| constrained_binding.binding) + .find_map(|constrained_binding| constrained_binding.binding.definition()) } } @@ -613,17 +601,17 @@ mod tests { TestCase { db, file } } - fn names(table: &SymbolTable) -> Vec { + fn names(table: &PlaceTable) -> Vec { table - .symbols() - .map(|symbol| symbol.name().to_string()) + .places() + .filter_map(|expr| Some(expr.as_name()?.to_string())) .collect() } #[test] fn empty() { let TestCase { db, file } = test_case(""); - let global_table = symbol_table(&db, global_scope(&db, file)); + let global_table = place_table(&db, global_scope(&db, file)); let global_names = names(global_table); @@ -633,7 +621,7 @@ mod tests { #[test] fn simple() { let TestCase { db, file } = test_case("x"); - let global_table = symbol_table(&db, global_scope(&db, file)); + let global_table = place_table(&db, global_scope(&db, file)); assert_eq!(names(global_table), vec!["x"]); } @@ -641,7 +629,7 @@ mod tests { #[test] fn annotation_only() { let TestCase { db, file } = test_case("x: int"); - let global_table = symbol_table(&db, global_scope(&db, file)); + let global_table = place_table(&db, global_scope(&db, file)); assert_eq!(names(global_table), vec!["int", "x"]); // TODO record definition @@ -651,10 +639,10 @@ mod tests { fn import() { let TestCase { db, file } = test_case("import foo"); let scope = global_scope(&db, file); - let global_table = symbol_table(&db, scope); + let global_table = place_table(&db, scope); assert_eq!(names(global_table), vec!["foo"]); - let foo = global_table.symbol_id_by_name("foo").unwrap(); + let foo = global_table.place_id_by_name("foo").unwrap(); let use_def = use_def_map(&db, scope); let binding = use_def.first_public_binding(foo).unwrap(); @@ -664,7 +652,7 @@ mod tests { #[test] fn import_sub() { let TestCase { db, file } = test_case("import foo.bar"); - let global_table = symbol_table(&db, global_scope(&db, file)); + let global_table = place_table(&db, global_scope(&db, file)); assert_eq!(names(global_table), vec!["foo"]); } @@ -672,7 +660,7 @@ mod tests { #[test] fn import_as() { let TestCase { db, file } = test_case("import foo.bar as baz"); - let global_table = symbol_table(&db, global_scope(&db, file)); + let global_table = place_table(&db, global_scope(&db, file)); assert_eq!(names(global_table), vec!["baz"]); } @@ -681,12 +669,12 @@ mod tests { fn import_from() { let TestCase { db, file } = test_case("from bar import foo"); let scope = global_scope(&db, file); - let global_table = symbol_table(&db, scope); + let global_table = place_table(&db, scope); assert_eq!(names(global_table), vec!["foo"]); assert!( global_table - .symbol_by_name("foo") + .place_by_name("foo") .is_some_and(|symbol| { symbol.is_bound() && !symbol.is_used() }), "symbols that are defined get the defined flag" ); @@ -695,7 +683,7 @@ mod tests { let binding = use_def .first_public_binding( global_table - .symbol_id_by_name("foo") + .place_id_by_name("foo") .expect("symbol to exist"), ) .unwrap(); @@ -706,18 +694,18 @@ mod tests { fn assign() { let TestCase { db, file } = test_case("x = foo"); let scope = global_scope(&db, file); - let global_table = symbol_table(&db, scope); + let global_table = place_table(&db, scope); assert_eq!(names(global_table), vec!["foo", "x"]); assert!( global_table - .symbol_by_name("foo") + .place_by_name("foo") .is_some_and(|symbol| { !symbol.is_bound() && symbol.is_used() }), "a symbol used but not bound in a scope should have only the used flag" ); let use_def = use_def_map(&db, scope); let binding = use_def - .first_public_binding(global_table.symbol_id_by_name("x").expect("symbol exists")) + .first_public_binding(global_table.place_id_by_name("x").expect("symbol exists")) .unwrap(); assert!(matches!(binding.kind(&db), DefinitionKind::Assignment(_))); } @@ -726,13 +714,13 @@ mod tests { fn augmented_assignment() { let TestCase { db, file } = test_case("x += 1"); let scope = global_scope(&db, file); - let global_table = symbol_table(&db, scope); + let global_table = place_table(&db, scope); assert_eq!(names(global_table), vec!["x"]); let use_def = use_def_map(&db, scope); let binding = use_def - .first_public_binding(global_table.symbol_id_by_name("x").unwrap()) + .first_public_binding(global_table.place_id_by_name("x").unwrap()) .unwrap(); assert!(matches!( @@ -750,7 +738,7 @@ class C: y = 2 ", ); - let global_table = symbol_table(&db, global_scope(&db, file)); + let global_table = place_table(&db, global_scope(&db, file)); assert_eq!(names(global_table), vec!["C", "y"]); @@ -765,12 +753,12 @@ y = 2 assert_eq!(class_scope.kind(), ScopeKind::Class); assert_eq!(class_scope_id.to_scope_id(&db, file).name(&db), "C"); - let class_table = index.symbol_table(class_scope_id); + let class_table = index.place_table(class_scope_id); assert_eq!(names(&class_table), vec!["x"]); let use_def = index.use_def_map(class_scope_id); let binding = use_def - .first_public_binding(class_table.symbol_id_by_name("x").expect("symbol exists")) + .first_public_binding(class_table.place_id_by_name("x").expect("symbol exists")) .unwrap(); assert!(matches!(binding.kind(&db), DefinitionKind::Assignment(_))); } @@ -785,7 +773,7 @@ y = 2 ", ); let index = semantic_index(&db, file); - let global_table = index.symbol_table(FileScopeId::global()); + let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(&global_table), vec!["func", "y"]); @@ -798,16 +786,12 @@ y = 2 assert_eq!(function_scope.kind(), ScopeKind::Function); assert_eq!(function_scope_id.to_scope_id(&db, file).name(&db), "func"); - let function_table = index.symbol_table(function_scope_id); + let function_table = index.place_table(function_scope_id); assert_eq!(names(&function_table), vec!["x"]); let use_def = index.use_def_map(function_scope_id); let binding = use_def - .first_public_binding( - function_table - .symbol_id_by_name("x") - .expect("symbol exists"), - ) + .first_public_binding(function_table.place_id_by_name("x").expect("symbol exists")) .unwrap(); assert!(matches!(binding.kind(&db), DefinitionKind::Assignment(_))); } @@ -822,7 +806,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): ); let index = semantic_index(&db, file); - let global_table = symbol_table(&db, global_scope(&db, file)); + let global_table = place_table(&db, global_scope(&db, file)); assert_eq!(names(global_table), vec!["str", "int", "f"]); @@ -833,7 +817,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): panic!("Expected a function scope") }; - let function_table = index.symbol_table(function_scope_id); + let function_table = index.place_table(function_scope_id); assert_eq!( names(&function_table), vec!["a", "b", "c", "d", "args", "kwargs"], @@ -844,7 +828,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): let binding = use_def .first_public_binding( function_table - .symbol_id_by_name(name) + .place_id_by_name(name) .expect("symbol exists"), ) .unwrap(); @@ -853,7 +837,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): let args_binding = use_def .first_public_binding( function_table - .symbol_id_by_name("args") + .place_id_by_name("args") .expect("symbol exists"), ) .unwrap(); @@ -864,7 +848,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): let kwargs_binding = use_def .first_public_binding( function_table - .symbol_id_by_name("kwargs") + .place_id_by_name("kwargs") .expect("symbol exists"), ) .unwrap(); @@ -879,7 +863,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): let TestCase { db, file } = test_case("lambda a, b, c=1, *args, d=2, **kwargs: None"); let index = semantic_index(&db, file); - let global_table = symbol_table(&db, global_scope(&db, file)); + let global_table = place_table(&db, global_scope(&db, file)); assert!(names(global_table).is_empty()); @@ -890,7 +874,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): panic!("Expected a lambda scope") }; - let lambda_table = index.symbol_table(lambda_scope_id); + let lambda_table = index.place_table(lambda_scope_id); assert_eq!( names(&lambda_table), vec!["a", "b", "c", "d", "args", "kwargs"], @@ -899,14 +883,14 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): let use_def = index.use_def_map(lambda_scope_id); for name in ["a", "b", "c", "d"] { let binding = use_def - .first_public_binding(lambda_table.symbol_id_by_name(name).expect("symbol exists")) + .first_public_binding(lambda_table.place_id_by_name(name).expect("symbol exists")) .unwrap(); assert!(matches!(binding.kind(&db), DefinitionKind::Parameter(_))); } let args_binding = use_def .first_public_binding( lambda_table - .symbol_id_by_name("args") + .place_id_by_name("args") .expect("symbol exists"), ) .unwrap(); @@ -917,7 +901,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): let kwargs_binding = use_def .first_public_binding( lambda_table - .symbol_id_by_name("kwargs") + .place_id_by_name("kwargs") .expect("symbol exists"), ) .unwrap(); @@ -938,7 +922,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): ); let index = semantic_index(&db, file); - let global_table = index.symbol_table(FileScopeId::global()); + let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(&global_table), vec!["iter1"]); @@ -955,7 +939,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): "" ); - let comprehension_symbol_table = index.symbol_table(comprehension_scope_id); + let comprehension_symbol_table = index.place_table(comprehension_scope_id); assert_eq!(names(&comprehension_symbol_table), vec!["x", "y"]); @@ -964,7 +948,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): let binding = use_def .first_public_binding( comprehension_symbol_table - .symbol_id_by_name(name) + .place_id_by_name(name) .expect("symbol exists"), ) .unwrap(); @@ -1031,7 +1015,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): ); let index = semantic_index(&db, file); - let global_table = index.symbol_table(FileScopeId::global()); + let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(&global_table), vec!["iter1"]); @@ -1048,7 +1032,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): "" ); - let comprehension_symbol_table = index.symbol_table(comprehension_scope_id); + let comprehension_symbol_table = index.place_table(comprehension_scope_id); assert_eq!(names(&comprehension_symbol_table), vec!["y", "iter2"]); @@ -1067,7 +1051,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): "" ); - let inner_comprehension_symbol_table = index.symbol_table(inner_comprehension_scope_id); + let inner_comprehension_symbol_table = index.place_table(inner_comprehension_scope_id); assert_eq!(names(&inner_comprehension_symbol_table), vec!["x"]); } @@ -1082,14 +1066,14 @@ with item1 as x, item2 as y: ); let index = semantic_index(&db, file); - let global_table = index.symbol_table(FileScopeId::global()); + let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(&global_table), vec!["item1", "x", "item2", "y"]); let use_def = index.use_def_map(FileScopeId::global()); for name in ["x", "y"] { let binding = use_def - .first_public_binding(global_table.symbol_id_by_name(name).expect("symbol exists")) + .first_public_binding(global_table.place_id_by_name(name).expect("symbol exists")) .expect("Expected with item definition for {name}"); assert!(matches!(binding.kind(&db), DefinitionKind::WithItem(_))); } @@ -1105,14 +1089,14 @@ with context() as (x, y): ); let index = semantic_index(&db, file); - let global_table = index.symbol_table(FileScopeId::global()); + let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(&global_table), vec!["context", "x", "y"]); let use_def = index.use_def_map(FileScopeId::global()); for name in ["x", "y"] { let binding = use_def - .first_public_binding(global_table.symbol_id_by_name(name).expect("symbol exists")) + .first_public_binding(global_table.place_id_by_name(name).expect("symbol exists")) .expect("Expected with item definition for {name}"); assert!(matches!(binding.kind(&db), DefinitionKind::WithItem(_))); } @@ -1129,7 +1113,7 @@ def func(): ", ); let index = semantic_index(&db, file); - let global_table = index.symbol_table(FileScopeId::global()); + let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(&global_table), vec!["func"]); let [ @@ -1148,8 +1132,8 @@ def func(): assert_eq!(func_scope_2.kind(), ScopeKind::Function); assert_eq!(func_scope2_id.to_scope_id(&db, file).name(&db), "func"); - let func1_table = index.symbol_table(func_scope1_id); - let func2_table = index.symbol_table(func_scope2_id); + let func1_table = index.place_table(func_scope1_id); + let func2_table = index.place_table(func_scope2_id); assert_eq!(names(&func1_table), vec!["x"]); assert_eq!(names(&func2_table), vec!["y"]); @@ -1157,7 +1141,7 @@ def func(): let binding = use_def .first_public_binding( global_table - .symbol_id_by_name("func") + .place_id_by_name("func") .expect("symbol exists"), ) .unwrap(); @@ -1174,7 +1158,7 @@ def func[T](): ); let index = semantic_index(&db, file); - let global_table = index.symbol_table(FileScopeId::global()); + let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(&global_table), vec!["func"]); @@ -1187,7 +1171,7 @@ def func[T](): assert_eq!(ann_scope.kind(), ScopeKind::Annotation); assert_eq!(ann_scope_id.to_scope_id(&db, file).name(&db), "func"); - let ann_table = index.symbol_table(ann_scope_id); + let ann_table = index.place_table(ann_scope_id); assert_eq!(names(&ann_table), vec!["T"]); let [(func_scope_id, func_scope)] = @@ -1197,7 +1181,7 @@ def func[T](): }; assert_eq!(func_scope.kind(), ScopeKind::Function); assert_eq!(func_scope_id.to_scope_id(&db, file).name(&db), "func"); - let func_table = index.symbol_table(func_scope_id); + let func_table = index.place_table(func_scope_id); assert_eq!(names(&func_table), vec!["x"]); } @@ -1211,7 +1195,7 @@ class C[T]: ); let index = semantic_index(&db, file); - let global_table = index.symbol_table(FileScopeId::global()); + let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(&global_table), vec!["C"]); @@ -1224,11 +1208,11 @@ class C[T]: assert_eq!(ann_scope.kind(), ScopeKind::Annotation); assert_eq!(ann_scope_id.to_scope_id(&db, file).name(&db), "C"); - let ann_table = index.symbol_table(ann_scope_id); + let ann_table = index.place_table(ann_scope_id); assert_eq!(names(&ann_table), vec!["T"]); assert!( ann_table - .symbol_by_name("T") + .place_by_name("T") .is_some_and(|s| s.is_bound() && !s.is_used()), "type parameters are defined by the scope that introduces them" ); @@ -1241,7 +1225,7 @@ class C[T]: assert_eq!(class_scope.kind(), ScopeKind::Class); assert_eq!(class_scope_id.to_scope_id(&db, file).name(&db), "C"); - assert_eq!(names(&index.symbol_table(class_scope_id)), vec!["x"]); + assert_eq!(names(&index.place_table(class_scope_id)), vec!["x"]); } #[test] @@ -1369,9 +1353,9 @@ match subject: ); let global_scope_id = global_scope(&db, file); - let global_table = symbol_table(&db, global_scope_id); + let global_table = place_table(&db, global_scope_id); - assert!(global_table.symbol_by_name("Foo").unwrap().is_used()); + assert!(global_table.place_by_name("Foo").unwrap().is_used()); assert_eq!( names(global_table), vec![ @@ -1395,7 +1379,7 @@ match subject: ("l", 1), ] { let binding = use_def - .first_public_binding(global_table.symbol_id_by_name(name).expect("symbol exists")) + .first_public_binding(global_table.place_id_by_name(name).expect("symbol exists")) .expect("Expected with item definition for {name}"); if let DefinitionKind::MatchPattern(pattern) = binding.kind(&db) { assert_eq!(pattern.index(), expected_index); @@ -1418,14 +1402,14 @@ match 1: ); let global_scope_id = global_scope(&db, file); - let global_table = symbol_table(&db, global_scope_id); + let global_table = place_table(&db, global_scope_id); assert_eq!(names(global_table), vec!["first", "second"]); let use_def = use_def_map(&db, global_scope_id); for (name, expected_index) in [("first", 0), ("second", 0)] { let binding = use_def - .first_public_binding(global_table.symbol_id_by_name(name).expect("symbol exists")) + .first_public_binding(global_table.place_id_by_name(name).expect("symbol exists")) .expect("Expected with item definition for {name}"); if let DefinitionKind::MatchPattern(pattern) = binding.kind(&db) { assert_eq!(pattern.index(), expected_index); @@ -1439,13 +1423,13 @@ match 1: fn for_loops_single_assignment() { let TestCase { db, file } = test_case("for x in a: pass"); let scope = global_scope(&db, file); - let global_table = symbol_table(&db, scope); + let global_table = place_table(&db, scope); assert_eq!(&names(global_table), &["a", "x"]); let use_def = use_def_map(&db, scope); let binding = use_def - .first_public_binding(global_table.symbol_id_by_name("x").unwrap()) + .first_public_binding(global_table.place_id_by_name("x").unwrap()) .unwrap(); assert!(matches!(binding.kind(&db), DefinitionKind::For(_))); @@ -1455,16 +1439,16 @@ match 1: fn for_loops_simple_unpacking() { let TestCase { db, file } = test_case("for (x, y) in a: pass"); let scope = global_scope(&db, file); - let global_table = symbol_table(&db, scope); + let global_table = place_table(&db, scope); assert_eq!(&names(global_table), &["a", "x", "y"]); let use_def = use_def_map(&db, scope); let x_binding = use_def - .first_public_binding(global_table.symbol_id_by_name("x").unwrap()) + .first_public_binding(global_table.place_id_by_name("x").unwrap()) .unwrap(); let y_binding = use_def - .first_public_binding(global_table.symbol_id_by_name("y").unwrap()) + .first_public_binding(global_table.place_id_by_name("y").unwrap()) .unwrap(); assert!(matches!(x_binding.kind(&db), DefinitionKind::For(_))); @@ -1475,13 +1459,13 @@ match 1: fn for_loops_complex_unpacking() { let TestCase { db, file } = test_case("for [((a,) b), (c, d)] in e: pass"); let scope = global_scope(&db, file); - let global_table = symbol_table(&db, scope); + let global_table = place_table(&db, scope); assert_eq!(&names(global_table), &["e", "a", "b", "c", "d"]); let use_def = use_def_map(&db, scope); let binding = use_def - .first_public_binding(global_table.symbol_id_by_name("a").unwrap()) + .first_public_binding(global_table.place_id_by_name("a").unwrap()) .unwrap(); assert!(matches!(binding.kind(&db), DefinitionKind::For(_))); diff --git a/crates/ty_python_semantic/src/semantic_index/ast_ids.rs b/crates/ty_python_semantic/src/semantic_index/ast_ids.rs index 0a18330569047..191be73c23244 100644 --- a/crates/ty_python_semantic/src/semantic_index/ast_ids.rs +++ b/crates/ty_python_semantic/src/semantic_index/ast_ids.rs @@ -6,14 +6,14 @@ use ruff_python_ast::ExprRef; use crate::Db; use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey; +use crate::semantic_index::place::ScopeId; use crate::semantic_index::semantic_index; -use crate::semantic_index::symbol::ScopeId; /// AST ids for a single scope. /// /// The motivation for building the AST ids per scope isn't about reducing invalidation because /// the struct changes whenever the parsed AST changes. Instead, it's mainly that we can -/// build the AST ids struct when building the symbol table and also keep the property that +/// build the AST ids struct when building the place table and also keep the property that /// IDs of outer scopes are unaffected by changes in inner scopes. /// /// For example, we don't want that adding new statements to `foo` changes the statement id of `x = foo()` in: @@ -28,7 +28,7 @@ use crate::semantic_index::symbol::ScopeId; pub(crate) struct AstIds { /// Maps expressions to their expression id. expressions_map: FxHashMap, - /// Maps expressions which "use" a symbol (that is, [`ast::ExprName`]) to a use id. + /// Maps expressions which "use" a place (that is, [`ast::ExprName`], [`ast::ExprAttribute`] or [`ast::ExprSubscript`]) to a use id. uses_map: FxHashMap, } @@ -49,7 +49,7 @@ fn ast_ids<'db>(db: &'db dyn Db, scope: ScopeId) -> &'db AstIds { semantic_index(db, scope.file(db)).ast_ids(scope.file_scope_id(db)) } -/// Uniquely identifies a use of a name in a [`crate::semantic_index::symbol::FileScopeId`]. +/// Uniquely identifies a use of a name in a [`crate::semantic_index::place::FileScopeId`]. #[newtype_index] pub struct ScopedUseId; @@ -72,6 +72,20 @@ impl HasScopedUseId for ast::ExprName { } } +impl HasScopedUseId for ast::ExprAttribute { + fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId { + let expression_ref = ExprRef::from(self); + expression_ref.scoped_use_id(db, scope) + } +} + +impl HasScopedUseId for ast::ExprSubscript { + fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId { + let expression_ref = ExprRef::from(self); + expression_ref.scoped_use_id(db, scope) + } +} + impl HasScopedUseId for ast::ExprRef<'_> { fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId { let ast_ids = ast_ids(db, scope); @@ -79,7 +93,7 @@ impl HasScopedUseId for ast::ExprRef<'_> { } } -/// Uniquely identifies an [`ast::Expr`] in a [`crate::semantic_index::symbol::FileScopeId`]. +/// Uniquely identifies an [`ast::Expr`] in a [`crate::semantic_index::place::FileScopeId`]. #[newtype_index] #[derive(salsa::Update)] pub struct ScopedExpressionId; diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index a5b0819cac345..db4cf3da50f08 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -24,24 +24,22 @@ use crate::semantic_index::SemanticIndex; use crate::semantic_index::ast_ids::AstIdsBuilder; use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey; use crate::semantic_index::definition::{ - AnnotatedAssignmentDefinitionKind, AnnotatedAssignmentDefinitionNodeRef, - AssignmentDefinitionKind, AssignmentDefinitionNodeRef, ComprehensionDefinitionKind, - ComprehensionDefinitionNodeRef, Definition, DefinitionCategory, DefinitionKind, - DefinitionNodeKey, DefinitionNodeRef, Definitions, ExceptHandlerDefinitionNodeRef, - ForStmtDefinitionKind, ForStmtDefinitionNodeRef, ImportDefinitionNodeRef, - ImportFromDefinitionNodeRef, MatchPatternDefinitionNodeRef, StarImportDefinitionNodeRef, - TargetKind, WithItemDefinitionKind, WithItemDefinitionNodeRef, + AnnotatedAssignmentDefinitionNodeRef, AssignmentDefinitionNodeRef, + ComprehensionDefinitionNodeRef, Definition, DefinitionCategory, DefinitionNodeKey, + DefinitionNodeRef, Definitions, ExceptHandlerDefinitionNodeRef, ForStmtDefinitionNodeRef, + ImportDefinitionNodeRef, ImportFromDefinitionNodeRef, MatchPatternDefinitionNodeRef, + StarImportDefinitionNodeRef, WithItemDefinitionNodeRef, }; use crate::semantic_index::expression::{Expression, ExpressionKind}; +use crate::semantic_index::place::{ + FileScopeId, NodeWithScopeKey, NodeWithScopeKind, NodeWithScopeRef, PlaceExpr, + PlaceTableBuilder, Scope, ScopeId, ScopeKind, ScopedPlaceId, +}; use crate::semantic_index::predicate::{ PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, ScopedPredicateId, StarImportPlaceholderPredicate, }; use crate::semantic_index::re_exports::exported_names; -use crate::semantic_index::symbol::{ - FileScopeId, NodeWithScopeKey, NodeWithScopeKind, NodeWithScopeRef, Scope, ScopeId, ScopeKind, - ScopedSymbolId, SymbolTableBuilder, -}; use crate::semantic_index::use_def::{ EagerSnapshotKey, FlowSnapshot, ScopedEagerSnapshotId, UseDefMapBuilder, }; @@ -100,13 +98,12 @@ pub(super) struct SemanticIndexBuilder<'db> { // Semantic Index fields scopes: IndexVec, scope_ids_by_scope: IndexVec>, - symbol_tables: IndexVec, - instance_attribute_tables: IndexVec, + place_tables: IndexVec, ast_ids: IndexVec, use_def_maps: IndexVec>, scopes_by_node: FxHashMap, scopes_by_expression: FxHashMap, - globals_by_scope: FxHashMap>, + globals_by_scope: FxHashMap>, definitions_by_node: FxHashMap>, expressions_by_node: FxHashMap>, imported_modules: FxHashSet, @@ -135,8 +132,7 @@ impl<'db> SemanticIndexBuilder<'db> { has_future_annotations: false, scopes: IndexVec::new(), - symbol_tables: IndexVec::new(), - instance_attribute_tables: IndexVec::new(), + place_tables: IndexVec::new(), ast_ids: IndexVec::new(), scope_ids_by_scope: IndexVec::new(), use_def_maps: IndexVec::new(), @@ -259,9 +255,7 @@ impl<'db> SemanticIndexBuilder<'db> { self.try_node_context_stack_manager.enter_nested_scope(); let file_scope_id = self.scopes.push(scope); - self.symbol_tables.push(SymbolTableBuilder::default()); - self.instance_attribute_tables - .push(SymbolTableBuilder::default()); + self.place_tables.push(PlaceTableBuilder::default()); self.use_def_maps .push(UseDefMapBuilder::new(is_class_scope)); let ast_id_scope = self.ast_ids.push(AstIdsBuilder::default()); @@ -301,36 +295,35 @@ impl<'db> SemanticIndexBuilder<'db> { // If the scope that we just popped off is an eager scope, we need to "lock" our view of // which bindings reach each of the uses in the scope. Loop through each enclosing scope, - // looking for any that bind each symbol. + // looking for any that bind each place. for enclosing_scope_info in self.scope_stack.iter().rev() { let enclosing_scope_id = enclosing_scope_info.file_scope_id; let enclosing_scope_kind = self.scopes[enclosing_scope_id].kind(); - let enclosing_symbol_table = &self.symbol_tables[enclosing_scope_id]; + let enclosing_place_table = &self.place_tables[enclosing_scope_id]; - for nested_symbol in self.symbol_tables[popped_scope_id].symbols() { - // Skip this symbol if this enclosing scope doesn't contain any bindings for it. - // Note that even if this symbol is bound in the popped scope, + for nested_place in self.place_tables[popped_scope_id].places() { + // Skip this place if this enclosing scope doesn't contain any bindings for it. + // Note that even if this place is bound in the popped scope, // it may refer to the enclosing scope bindings // so we also need to snapshot the bindings of the enclosing scope. - let Some(enclosing_symbol_id) = - enclosing_symbol_table.symbol_id_by_name(nested_symbol.name()) + let Some(enclosing_place_id) = enclosing_place_table.place_id_by_expr(nested_place) else { continue; }; - let enclosing_symbol = enclosing_symbol_table.symbol(enclosing_symbol_id); + let enclosing_place = enclosing_place_table.place_expr(enclosing_place_id); - // Snapshot the state of this symbol that are visible at this point in this + // Snapshot the state of this place that are visible at this point in this // enclosing scope. let key = EagerSnapshotKey { enclosing_scope: enclosing_scope_id, - enclosing_symbol: enclosing_symbol_id, + enclosing_place: enclosing_place_id, nested_scope: popped_scope_id, }; let eager_snapshot = self.use_def_maps[enclosing_scope_id].snapshot_eager_state( - enclosing_symbol_id, + enclosing_place_id, enclosing_scope_kind, - enclosing_symbol.is_bound(), + enclosing_place, ); self.eager_snapshots.insert(key, eager_snapshot); } @@ -338,7 +331,7 @@ impl<'db> SemanticIndexBuilder<'db> { // Lazy scopes are "sticky": once we see a lazy scope we stop doing lookups // eagerly, even if we would encounter another eager enclosing scope later on. // Also, narrowing constraints outside a lazy scope are not applicable. - // TODO: If the symbol has never been rewritten, they are applicable. + // TODO: If the place has never been rewritten, they are applicable. if !enclosing_scope_kind.is_eager() { break; } @@ -347,14 +340,9 @@ impl<'db> SemanticIndexBuilder<'db> { popped_scope_id } - fn current_symbol_table(&mut self) -> &mut SymbolTableBuilder { - let scope_id = self.current_scope(); - &mut self.symbol_tables[scope_id] - } - - fn current_attribute_table(&mut self) -> &mut SymbolTableBuilder { + fn current_place_table(&mut self) -> &mut PlaceTableBuilder { let scope_id = self.current_scope(); - &mut self.instance_attribute_tables[scope_id] + &mut self.place_tables[scope_id] } fn current_use_def_map_mut(&mut self) -> &mut UseDefMapBuilder<'db> { @@ -389,34 +377,36 @@ impl<'db> SemanticIndexBuilder<'db> { self.current_use_def_map_mut().merge(state); } - /// Add a symbol to the symbol table and the use-def map. - /// Return the [`ScopedSymbolId`] that uniquely identifies the symbol in both. - fn add_symbol(&mut self, name: Name) -> ScopedSymbolId { - let (symbol_id, added) = self.current_symbol_table().add_symbol(name); + /// Add a symbol to the place table and the use-def map. + /// Return the [`ScopedPlaceId`] that uniquely identifies the symbol in both. + fn add_symbol(&mut self, name: Name) -> ScopedPlaceId { + let (place_id, added) = self.current_place_table().add_symbol(name); if added { - self.current_use_def_map_mut().add_symbol(symbol_id); + self.current_use_def_map_mut().add_place(place_id); } - symbol_id + place_id } - fn add_attribute(&mut self, name: Name) -> ScopedSymbolId { - let (symbol_id, added) = self.current_attribute_table().add_symbol(name); + /// Add a place to the place table and the use-def map. + /// Return the [`ScopedPlaceId`] that uniquely identifies the place in both. + fn add_place(&mut self, place_expr: PlaceExpr) -> ScopedPlaceId { + let (place_id, added) = self.current_place_table().add_place(place_expr); if added { - self.current_use_def_map_mut().add_attribute(symbol_id); + self.current_use_def_map_mut().add_place(place_id); } - symbol_id + place_id } - fn mark_symbol_bound(&mut self, id: ScopedSymbolId) { - self.current_symbol_table().mark_symbol_bound(id); + fn mark_place_bound(&mut self, id: ScopedPlaceId) { + self.current_place_table().mark_place_bound(id); } - fn mark_symbol_declared(&mut self, id: ScopedSymbolId) { - self.current_symbol_table().mark_symbol_declared(id); + fn mark_place_declared(&mut self, id: ScopedPlaceId) { + self.current_place_table().mark_place_declared(id); } - fn mark_symbol_used(&mut self, id: ScopedSymbolId) { - self.current_symbol_table().mark_symbol_used(id); + fn mark_place_used(&mut self, id: ScopedPlaceId) { + self.current_place_table().mark_place_used(id); } fn add_entry_for_definition_key(&mut self, key: DefinitionNodeKey) -> &mut Definitions<'db> { @@ -432,11 +422,10 @@ impl<'db> SemanticIndexBuilder<'db> { /// for all nodes *except* [`ast::Alias`] nodes representing `*` imports. fn add_definition( &mut self, - symbol: ScopedSymbolId, + place: ScopedPlaceId, definition_node: impl Into> + std::fmt::Debug + Copy, ) -> Definition<'db> { - let (definition, num_definitions) = - self.push_additional_definition(symbol, definition_node); + let (definition, num_definitions) = self.push_additional_definition(place, definition_node); debug_assert_eq!( num_definitions, 1, "Attempted to create multiple `Definition`s associated with AST node {definition_node:?}" @@ -444,6 +433,22 @@ impl<'db> SemanticIndexBuilder<'db> { definition } + fn delete_associated_bindings(&mut self, place: ScopedPlaceId) { + let scope = self.current_scope(); + // Don't delete associated bindings if the scope is a class scope & place is a name (it's never visible to nested scopes) + if self.scopes[scope].kind() == ScopeKind::Class + && self.place_tables[scope].place_expr(place).is_name() + { + return; + } + for associated_place in self.place_tables[scope].associated_place_ids(place) { + let is_place_name = self.place_tables[scope] + .place_expr(associated_place) + .is_name(); + self.use_def_maps[scope].delete_binding(associated_place, is_place_name); + } + } + /// Push a new [`Definition`] onto the list of definitions /// associated with the `definition_node` AST node. /// @@ -457,7 +462,7 @@ impl<'db> SemanticIndexBuilder<'db> { /// prefer to use `self.add_definition()`, which ensures that this invariant is maintained. fn push_additional_definition( &mut self, - symbol: ScopedSymbolId, + place: ScopedPlaceId, definition_node: impl Into>, ) -> (Definition<'db>, usize) { let definition_node: DefinitionNodeRef<'_> = definition_node.into(); @@ -471,7 +476,7 @@ impl<'db> SemanticIndexBuilder<'db> { self.db, self.file, self.current_scope(), - symbol, + place, kind, is_reexported, countme::Count::default(), @@ -484,19 +489,24 @@ impl<'db> SemanticIndexBuilder<'db> { }; if category.is_binding() { - self.mark_symbol_bound(symbol); + self.mark_place_bound(place); } if category.is_declaration() { - self.mark_symbol_declared(symbol); + self.mark_place_declared(place); } + let is_place_name = self.current_place_table().place_expr(place).is_name(); let use_def = self.current_use_def_map_mut(); match category { DefinitionCategory::DeclarationAndBinding => { - use_def.record_declaration_and_binding(symbol, definition); + use_def.record_declaration_and_binding(place, definition, is_place_name); + self.delete_associated_bindings(place); + } + DefinitionCategory::Declaration => use_def.record_declaration(place, definition), + DefinitionCategory::Binding => { + use_def.record_binding(place, definition, is_place_name); + self.delete_associated_bindings(place); } - DefinitionCategory::Declaration => use_def.record_declaration(symbol, definition), - DefinitionCategory::Binding => use_def.record_binding(symbol, definition), } let mut try_node_stack_manager = std::mem::take(&mut self.try_node_context_stack_manager); @@ -506,25 +516,6 @@ impl<'db> SemanticIndexBuilder<'db> { (definition, num_definitions) } - fn add_attribute_definition( - &mut self, - symbol: ScopedSymbolId, - definition_kind: DefinitionKind<'db>, - ) -> Definition { - let definition = Definition::new( - self.db, - self.file, - self.current_scope(), - symbol, - definition_kind, - false, - countme::Count::default(), - ); - self.current_use_def_map_mut() - .record_attribute_binding(symbol, definition); - definition - } - fn record_expression_narrowing_constraint( &mut self, precide_node: &ast::Expr, @@ -684,28 +675,6 @@ impl<'db> SemanticIndexBuilder<'db> { self.current_assignments.last_mut() } - /// Records the fact that we saw an attribute assignment of the form - /// `object.attr: ( = …)` or `object.attr = `. - fn register_attribute_assignment( - &mut self, - object: &ast::Expr, - attr: &'db ast::Identifier, - definition_kind: DefinitionKind<'db>, - ) { - if self.is_method_of_class().is_some() { - // We only care about attribute assignments to the first parameter of a method, - // i.e. typically `self` or `cls`. - let accessed_object_refers_to_first_parameter = - object.as_name_expr().map(|name| name.id.as_str()) - == self.current_first_parameter_name; - - if accessed_object_refers_to_first_parameter { - let symbol = self.add_attribute(attr.id().clone()); - self.add_attribute_definition(symbol, definition_kind); - } - } - } - fn predicate_kind(&mut self, pattern: &ast::Pattern) -> PatternPredicateKind<'db> { match pattern { ast::Pattern::MatchValue(pattern) => { @@ -850,8 +819,8 @@ impl<'db> SemanticIndexBuilder<'db> { // TODO create Definition for PEP 695 typevars // note that the "bound" on the typevar is a totally different thing than whether // or not a name is "bound" by a typevar declaration; the latter is always true. - self.mark_symbol_bound(symbol); - self.mark_symbol_declared(symbol); + self.mark_place_bound(symbol); + self.mark_place_declared(symbol); if let Some(bounds) = bound { self.visit_expr(bounds); } @@ -1022,7 +991,7 @@ impl<'db> SemanticIndexBuilder<'db> { )); Some(unpackable.as_current_assignment(unpack)) } - ast::Expr::Name(_) | ast::Expr::Attribute(_) => { + ast::Expr::Name(_) | ast::Expr::Attribute(_) | ast::Expr::Subscript(_) => { Some(unpackable.as_current_assignment(None)) } _ => None, @@ -1050,18 +1019,12 @@ impl<'db> SemanticIndexBuilder<'db> { assert_eq!(&self.current_assignments, &[]); - let mut symbol_tables: IndexVec<_, _> = self - .symbol_tables + let mut place_tables: IndexVec<_, _> = self + .place_tables .into_iter() .map(|builder| Arc::new(builder.finish())) .collect(); - let mut instance_attribute_tables: IndexVec<_, _> = self - .instance_attribute_tables - .into_iter() - .map(SymbolTableBuilder::finish) - .collect(); - let mut use_def_maps: IndexVec<_, _> = self .use_def_maps .into_iter() @@ -1075,8 +1038,7 @@ impl<'db> SemanticIndexBuilder<'db> { .collect(); self.scopes.shrink_to_fit(); - symbol_tables.shrink_to_fit(); - instance_attribute_tables.shrink_to_fit(); + place_tables.shrink_to_fit(); use_def_maps.shrink_to_fit(); ast_ids.shrink_to_fit(); self.scopes_by_expression.shrink_to_fit(); @@ -1089,8 +1051,7 @@ impl<'db> SemanticIndexBuilder<'db> { self.globals_by_scope.shrink_to_fit(); SemanticIndex { - symbol_tables, - instance_attribute_tables, + place_tables, scopes: self.scopes, definitions_by_node: self.definitions_by_node, expressions_by_node: self.expressions_by_node, @@ -1213,7 +1174,7 @@ where // used to collect all the overloaded definitions of a function. This needs to be // done on the `Identifier` node as opposed to `ExprName` because that's what the // AST uses. - self.mark_symbol_used(symbol); + self.mark_place_used(symbol); let use_id = self.current_ast_ids().record_use(name); self.current_use_def_map_mut() .record_use(symbol, use_id, NodeKey::from_node(name)); @@ -1356,7 +1317,10 @@ where // For more details, see the doc-comment on `StarImportPlaceholderPredicate`. for export in exported_names(self.db, referenced_module) { let symbol_id = self.add_symbol(export.clone()); - let node_ref = StarImportDefinitionNodeRef { node, symbol_id }; + let node_ref = StarImportDefinitionNodeRef { + node, + place_id: symbol_id, + }; let star_import = StarImportPlaceholderPredicate::new( self.db, self.file, @@ -1365,7 +1329,7 @@ where ); let pre_definition = - self.current_use_def_map().single_symbol_snapshot(symbol_id); + self.current_use_def_map().single_place_snapshot(symbol_id); self.push_additional_definition(symbol_id, node_ref); self.current_use_def_map_mut() .record_and_negate_star_import_visibility_constraint( @@ -1920,8 +1884,8 @@ where ast::Stmt::Global(ast::StmtGlobal { range: _, names }) => { for name in names { let symbol_id = self.add_symbol(name.id.clone()); - let symbol_table = self.current_symbol_table(); - let symbol = symbol_table.symbol(symbol_id); + let symbol_table = self.current_place_table(); + let symbol = symbol_table.place_expr(symbol_id); if symbol.is_bound() || symbol.is_declared() || symbol.is_used() { self.report_semantic_error(SemanticSyntaxError { kind: SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration { @@ -1942,9 +1906,9 @@ where } ast::Stmt::Delete(ast::StmtDelete { targets, range: _ }) => { for target in targets { - if let ast::Expr::Name(ast::ExprName { id, .. }) = target { - let symbol_id = self.add_symbol(id.clone()); - self.current_symbol_table().mark_symbol_used(symbol_id); + if let Ok(target) = PlaceExpr::try_from(target) { + let place_id = self.add_place(target); + self.current_place_table().mark_place_used(place_id); } } walk_stmt(self, stmt); @@ -1971,109 +1935,133 @@ where let node_key = NodeKey::from_node(expr); match expr { - ast::Expr::Name(ast::ExprName { id, ctx, .. }) => { - let (is_use, is_definition) = match (ctx, self.current_assignment()) { - (ast::ExprContext::Store, Some(CurrentAssignment::AugAssign(_))) => { - // For augmented assignment, the target expression is also used. - (true, true) + ast::Expr::Name(ast::ExprName { ctx, .. }) + | ast::Expr::Attribute(ast::ExprAttribute { ctx, .. }) + | ast::Expr::Subscript(ast::ExprSubscript { ctx, .. }) => { + if let Ok(mut place_expr) = PlaceExpr::try_from(expr) { + if self.is_method_of_class().is_some() { + // We specifically mark attribute assignments to the first parameter of a method, + // i.e. typically `self` or `cls`. + let accessed_object_refers_to_first_parameter = self + .current_first_parameter_name + .is_some_and(|fst| place_expr.root_name().as_str() == fst); + + if accessed_object_refers_to_first_parameter && place_expr.is_member() { + place_expr.mark_instance_attribute(); + } } - (ast::ExprContext::Load, _) => (true, false), - (ast::ExprContext::Store, _) => (false, true), - (ast::ExprContext::Del, _) => (false, true), - (ast::ExprContext::Invalid, _) => (false, false), - }; - let symbol = self.add_symbol(id.clone()); - if is_use { - self.mark_symbol_used(symbol); - let use_id = self.current_ast_ids().record_use(expr); - self.current_use_def_map_mut() - .record_use(symbol, use_id, node_key); - } - - if is_definition { - match self.current_assignment() { - Some(CurrentAssignment::Assign { node, unpack }) => { - self.add_definition( - symbol, - AssignmentDefinitionNodeRef { - unpack, - value: &node.value, - target: expr, - }, - ); - } - Some(CurrentAssignment::AnnAssign(ann_assign)) => { - self.add_definition( - symbol, - AnnotatedAssignmentDefinitionNodeRef { - node: ann_assign, - annotation: &ann_assign.annotation, - value: ann_assign.value.as_deref(), - target: expr, - }, - ); - } - Some(CurrentAssignment::AugAssign(aug_assign)) => { - self.add_definition(symbol, aug_assign); + let (is_use, is_definition) = match (ctx, self.current_assignment()) { + (ast::ExprContext::Store, Some(CurrentAssignment::AugAssign(_))) => { + // For augmented assignment, the target expression is also used. + (true, true) } - Some(CurrentAssignment::For { node, unpack }) => { - self.add_definition( - symbol, - ForStmtDefinitionNodeRef { - unpack, - iterable: &node.iter, - target: expr, - is_async: node.is_async, - }, - ); - } - Some(CurrentAssignment::Named(named)) => { - // TODO(dhruvmanila): If the current scope is a comprehension, then the - // named expression is implicitly nonlocal. This is yet to be - // implemented. - self.add_definition(symbol, named); - } - Some(CurrentAssignment::Comprehension { - unpack, - node, - first, - }) => { - self.add_definition( - symbol, - ComprehensionDefinitionNodeRef { - unpack, - iterable: &node.iter, - target: expr, - first, - is_async: node.is_async, - }, - ); - } - Some(CurrentAssignment::WithItem { - item, - is_async, - unpack, - }) => { - self.add_definition( - symbol, - WithItemDefinitionNodeRef { - unpack, - context_expr: &item.context_expr, - target: expr, - is_async, - }, - ); + (ast::ExprContext::Load, _) => (true, false), + (ast::ExprContext::Store, _) => (false, true), + (ast::ExprContext::Del, _) => (false, true), + (ast::ExprContext::Invalid, _) => (false, false), + }; + let place_id = self.add_place(place_expr); + + if is_use { + self.mark_place_used(place_id); + let use_id = self.current_ast_ids().record_use(expr); + self.current_use_def_map_mut() + .record_use(place_id, use_id, node_key); + } + + if is_definition { + match self.current_assignment() { + Some(CurrentAssignment::Assign { node, unpack }) => { + self.add_definition( + place_id, + AssignmentDefinitionNodeRef { + unpack, + value: &node.value, + target: expr, + }, + ); + } + Some(CurrentAssignment::AnnAssign(ann_assign)) => { + self.add_standalone_type_expression(&ann_assign.annotation); + self.add_definition( + place_id, + AnnotatedAssignmentDefinitionNodeRef { + node: ann_assign, + annotation: &ann_assign.annotation, + value: ann_assign.value.as_deref(), + target: expr, + }, + ); + } + Some(CurrentAssignment::AugAssign(aug_assign)) => { + self.add_definition(place_id, aug_assign); + } + Some(CurrentAssignment::For { node, unpack }) => { + self.add_definition( + place_id, + ForStmtDefinitionNodeRef { + unpack, + iterable: &node.iter, + target: expr, + is_async: node.is_async, + }, + ); + } + Some(CurrentAssignment::Named(named)) => { + // TODO(dhruvmanila): If the current scope is a comprehension, then the + // named expression is implicitly nonlocal. This is yet to be + // implemented. + self.add_definition(place_id, named); + } + Some(CurrentAssignment::Comprehension { + unpack, + node, + first, + }) => { + self.add_definition( + place_id, + ComprehensionDefinitionNodeRef { + unpack, + iterable: &node.iter, + target: expr, + first, + is_async: node.is_async, + }, + ); + } + Some(CurrentAssignment::WithItem { + item, + is_async, + unpack, + }) => { + self.add_definition( + place_id, + WithItemDefinitionNodeRef { + unpack, + context_expr: &item.context_expr, + target: expr, + is_async, + }, + ); + } + None => {} } - None => {} + } + + if let Some(unpack_position) = self + .current_assignment_mut() + .and_then(CurrentAssignment::unpack_position_mut) + { + *unpack_position = UnpackPosition::Other; } } - if let Some(unpack_position) = self - .current_assignment_mut() - .and_then(CurrentAssignment::unpack_position_mut) - { - *unpack_position = UnpackPosition::Other; + // Track reachability of attribute expressions to silence `unresolved-attribute` + // diagnostics in unreachable code. + if expr.is_attribute_expr() { + self.current_use_def_map_mut() + .record_node_reachability(node_key); } walk_expr(self, expr); @@ -2239,125 +2227,6 @@ where self.simplify_visibility_constraints(pre_op); } - ast::Expr::Attribute(ast::ExprAttribute { - value: object, - attr, - ctx, - range: _, - }) => { - if ctx.is_store() { - match self.current_assignment() { - Some(CurrentAssignment::Assign { node, unpack, .. }) => { - // SAFETY: `value` and `expr` belong to the `self.module` tree - #[expect(unsafe_code)] - let assignment = AssignmentDefinitionKind::new( - TargetKind::from(unpack), - unsafe { AstNodeRef::new(self.module.clone(), &node.value) }, - unsafe { AstNodeRef::new(self.module.clone(), expr) }, - ); - self.register_attribute_assignment( - object, - attr, - DefinitionKind::Assignment(assignment), - ); - } - Some(CurrentAssignment::AnnAssign(ann_assign)) => { - self.add_standalone_type_expression(&ann_assign.annotation); - // SAFETY: `annotation`, `value` and `expr` belong to the `self.module` tree - #[expect(unsafe_code)] - let assignment = AnnotatedAssignmentDefinitionKind::new( - unsafe { - AstNodeRef::new(self.module.clone(), &ann_assign.annotation) - }, - ann_assign.value.as_deref().map(|value| unsafe { - AstNodeRef::new(self.module.clone(), value) - }), - unsafe { AstNodeRef::new(self.module.clone(), expr) }, - ); - self.register_attribute_assignment( - object, - attr, - DefinitionKind::AnnotatedAssignment(assignment), - ); - } - Some(CurrentAssignment::For { node, unpack, .. }) => { - // // SAFETY: `iter` and `expr` belong to the `self.module` tree - #[expect(unsafe_code)] - let assignment = ForStmtDefinitionKind::new( - TargetKind::from(unpack), - unsafe { AstNodeRef::new(self.module.clone(), &node.iter) }, - unsafe { AstNodeRef::new(self.module.clone(), expr) }, - node.is_async, - ); - self.register_attribute_assignment( - object, - attr, - DefinitionKind::For(assignment), - ); - } - Some(CurrentAssignment::WithItem { - item, - unpack, - is_async, - .. - }) => { - // SAFETY: `context_expr` and `expr` belong to the `self.module` tree - #[expect(unsafe_code)] - let assignment = WithItemDefinitionKind::new( - TargetKind::from(unpack), - unsafe { AstNodeRef::new(self.module.clone(), &item.context_expr) }, - unsafe { AstNodeRef::new(self.module.clone(), expr) }, - is_async, - ); - self.register_attribute_assignment( - object, - attr, - DefinitionKind::WithItem(assignment), - ); - } - Some(CurrentAssignment::Comprehension { - unpack, - node, - first, - }) => { - // SAFETY: `iter` and `expr` belong to the `self.module` tree - #[expect(unsafe_code)] - let assignment = ComprehensionDefinitionKind { - target_kind: TargetKind::from(unpack), - iterable: unsafe { - AstNodeRef::new(self.module.clone(), &node.iter) - }, - target: unsafe { AstNodeRef::new(self.module.clone(), expr) }, - first, - is_async: node.is_async, - }; - // Temporarily move to the scope of the method to which the instance attribute is defined. - // SAFETY: `self.scope_stack` is not empty because the targets in comprehensions should always introduce a new scope. - let scope = self.scope_stack.pop().expect("The popped scope must be a comprehension, which must have a parent scope"); - self.register_attribute_assignment( - object, - attr, - DefinitionKind::Comprehension(assignment), - ); - self.scope_stack.push(scope); - } - Some(CurrentAssignment::AugAssign(_)) => { - // TODO: - } - Some(CurrentAssignment::Named(_)) => { - // A named expression whose target is an attribute is syntactically prohibited - } - None => {} - } - } - - // Track reachability of attribute expressions to silence `unresolved-attribute` - // diagnostics in unreachable code. - self.current_use_def_map_mut() - .record_node_reachability(node_key); - - walk_expr(self, expr); - } ast::Expr::StringLiteral(_) => { // Track reachability of string literals, as they could be a stringified annotation // with child expressions whose reachability we are interested in. diff --git a/crates/ty_python_semantic/src/semantic_index/definition.rs b/crates/ty_python_semantic/src/semantic_index/definition.rs index c8720d7cf77d0..3adbeb68c6305 100644 --- a/crates/ty_python_semantic/src/semantic_index/definition.rs +++ b/crates/ty_python_semantic/src/semantic_index/definition.rs @@ -8,16 +8,16 @@ use ruff_text_size::{Ranged, TextRange}; use crate::Db; use crate::ast_node_ref::AstNodeRef; use crate::node_key::NodeKey; -use crate::semantic_index::symbol::{FileScopeId, ScopeId, ScopedSymbolId}; +use crate::semantic_index::place::{FileScopeId, ScopeId, ScopedPlaceId}; use crate::unpack::{Unpack, UnpackPosition}; -/// A definition of a symbol. +/// A definition of a place. /// /// ## ID stability /// The `Definition`'s ID is stable when the only field that change is its `kind` (AST node). /// -/// The `Definition` changes when the `file`, `scope`, or `symbol` change. This can be -/// because a new scope gets inserted before the `Definition` or a new symbol is inserted +/// The `Definition` changes when the `file`, `scope`, or `place` change. This can be +/// because a new scope gets inserted before the `Definition` or a new place is inserted /// before this `Definition`. However, the ID can be considered stable and it is okay to use /// `Definition` in cross-module` salsa queries or as a field on other salsa tracked structs. #[salsa::tracked(debug)] @@ -28,8 +28,8 @@ pub struct Definition<'db> { /// The scope in which the definition occurs. pub(crate) file_scope: FileScopeId, - /// The symbol defined. - pub(crate) symbol: ScopedSymbolId, + /// The place ID of the definition. + pub(crate) place: ScopedPlaceId, /// WARNING: Only access this field when doing type inference for the same /// file as where `Definition` is defined to avoid cross-file query dependencies. @@ -89,6 +89,39 @@ impl<'a, 'db> IntoIterator for &'a Definitions<'db> { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, salsa::Update)] +pub(crate) enum DefinitionState<'db> { + Defined(Definition<'db>), + /// Represents the implicit "unbound"/"undeclared" definition of every place. + Undefined, + /// Represents a definition that has been deleted. + /// This used when an attribute/subscript definition (such as `x.y = ...`, `x[0] = ...`) becomes obsolete due to a reassignment of the root place. + Deleted, +} + +impl<'db> DefinitionState<'db> { + pub(crate) fn is_defined_and(self, f: impl Fn(Definition<'db>) -> bool) -> bool { + matches!(self, DefinitionState::Defined(def) if f(def)) + } + + pub(crate) fn is_undefined_or(self, f: impl Fn(Definition<'db>) -> bool) -> bool { + matches!(self, DefinitionState::Undefined) + || matches!(self, DefinitionState::Defined(def) if f(def)) + } + + pub(crate) fn is_undefined(self) -> bool { + matches!(self, DefinitionState::Undefined) + } + + #[allow(unused)] + pub(crate) fn definition(self) -> Option> { + match self { + DefinitionState::Defined(def) => Some(def), + DefinitionState::Deleted | DefinitionState::Undefined => None, + } + } +} + #[derive(Copy, Clone, Debug)] pub(crate) enum DefinitionNodeRef<'a> { Import(ImportDefinitionNodeRef<'a>), @@ -232,7 +265,7 @@ pub(crate) struct ImportDefinitionNodeRef<'a> { #[derive(Copy, Clone, Debug)] pub(crate) struct StarImportDefinitionNodeRef<'a> { pub(crate) node: &'a ast::StmtImportFrom, - pub(crate) symbol_id: ScopedSymbolId, + pub(crate) place_id: ScopedPlaceId, } #[derive(Copy, Clone, Debug)] @@ -323,10 +356,10 @@ impl<'db> DefinitionNodeRef<'db> { is_reexported, }), DefinitionNodeRef::ImportStar(star_import) => { - let StarImportDefinitionNodeRef { node, symbol_id } = star_import; + let StarImportDefinitionNodeRef { node, place_id } = star_import; DefinitionKind::StarImport(StarImportDefinitionKind { node: unsafe { AstNodeRef::new(parsed, node) }, - symbol_id, + place_id, }) } DefinitionNodeRef::Function(function) => { @@ -456,7 +489,7 @@ impl<'db> DefinitionNodeRef<'db> { // INVARIANT: for an invalid-syntax statement such as `from foo import *, bar, *`, // we only create a `StarImportDefinitionKind` for the *first* `*` alias in the names list. - Self::ImportStar(StarImportDefinitionNodeRef { node, symbol_id: _ }) => node + Self::ImportStar(StarImportDefinitionNodeRef { node, place_id: _ }) => node .names .iter() .find(|alias| &alias.name == "*") @@ -517,7 +550,7 @@ pub(crate) enum DefinitionCategory { } impl DefinitionCategory { - /// True if this definition establishes a "declared type" for the symbol. + /// True if this definition establishes a "declared type" for the place. /// /// If so, any assignments reached by this definition are in error if they assign a value of a /// type not assignable to the declared type. @@ -530,7 +563,7 @@ impl DefinitionCategory { ) } - /// True if this definition assigns a value to the symbol. + /// True if this definition assigns a value to the place. /// /// False only for annotated assignments without a RHS. pub(crate) fn is_binding(self) -> bool { @@ -591,8 +624,8 @@ impl DefinitionKind<'_> { /// Returns the [`TextRange`] of the definition target. /// - /// A definition target would mainly be the node representing the symbol being defined i.e., - /// [`ast::ExprName`] or [`ast::Identifier`] but could also be other nodes. + /// A definition target would mainly be the node representing the place being defined i.e., + /// [`ast::ExprName`], [`ast::Identifier`], [`ast::ExprAttribute`] or [`ast::ExprSubscript`] but could also be other nodes. pub(crate) fn target_range(&self) -> TextRange { match self { DefinitionKind::Import(import) => import.alias().range(), @@ -700,14 +733,15 @@ impl DefinitionKind<'_> { #[derive(Copy, Clone, Debug, PartialEq, Hash)] pub(crate) enum TargetKind<'db> { Sequence(UnpackPosition, Unpack<'db>), - NameOrAttribute, + /// Name, attribute, or subscript. + Single, } impl<'db> From)>> for TargetKind<'db> { fn from(value: Option<(UnpackPosition, Unpack<'db>)>) -> Self { match value { Some((unpack_position, unpack)) => TargetKind::Sequence(unpack_position, unpack), - None => TargetKind::NameOrAttribute, + None => TargetKind::Single, } } } @@ -715,7 +749,7 @@ impl<'db> From)>> for TargetKind<'db> { #[derive(Clone, Debug)] pub struct StarImportDefinitionKind { node: AstNodeRef, - symbol_id: ScopedSymbolId, + place_id: ScopedPlaceId, } impl StarImportDefinitionKind { @@ -737,8 +771,8 @@ impl StarImportDefinitionKind { ) } - pub(crate) fn symbol_id(&self) -> ScopedSymbolId { - self.symbol_id + pub(crate) fn place_id(&self) -> ScopedPlaceId { + self.place_id } } @@ -759,13 +793,18 @@ impl MatchPatternDefinitionKind { } } +/// Note that the elements of a comprehension can be in different scopes. +/// If the definition target of a comprehension is a name, it is in the comprehension's scope. +/// But if the target is an attribute or subscript, its definition is not in the comprehension's scope; +/// it is in the scope in which the root variable is bound. +/// TODO: currently we don't model this correctly and simply assume that it is in a scope outside the comprehension. #[derive(Clone, Debug)] pub struct ComprehensionDefinitionKind<'db> { - pub(super) target_kind: TargetKind<'db>, - pub(super) iterable: AstNodeRef, - pub(super) target: AstNodeRef, - pub(super) first: bool, - pub(super) is_async: bool, + target_kind: TargetKind<'db>, + iterable: AstNodeRef, + target: AstNodeRef, + first: bool, + is_async: bool, } impl<'db> ComprehensionDefinitionKind<'db> { @@ -840,18 +879,6 @@ pub struct AssignmentDefinitionKind<'db> { } impl<'db> AssignmentDefinitionKind<'db> { - pub(crate) fn new( - target_kind: TargetKind<'db>, - value: AstNodeRef, - target: AstNodeRef, - ) -> Self { - Self { - target_kind, - value, - target, - } - } - pub(crate) fn target_kind(&self) -> TargetKind<'db> { self.target_kind } @@ -873,18 +900,6 @@ pub struct AnnotatedAssignmentDefinitionKind { } impl AnnotatedAssignmentDefinitionKind { - pub(crate) fn new( - annotation: AstNodeRef, - value: Option>, - target: AstNodeRef, - ) -> Self { - Self { - annotation, - value, - target, - } - } - pub(crate) fn value(&self) -> Option<&ast::Expr> { self.value.as_deref() } @@ -907,20 +922,6 @@ pub struct WithItemDefinitionKind<'db> { } impl<'db> WithItemDefinitionKind<'db> { - pub(crate) fn new( - target_kind: TargetKind<'db>, - context_expr: AstNodeRef, - target: AstNodeRef, - is_async: bool, - ) -> Self { - Self { - target_kind, - context_expr, - target, - is_async, - } - } - pub(crate) fn context_expr(&self) -> &ast::Expr { self.context_expr.node() } @@ -947,20 +948,6 @@ pub struct ForStmtDefinitionKind<'db> { } impl<'db> ForStmtDefinitionKind<'db> { - pub(crate) fn new( - target_kind: TargetKind<'db>, - iterable: AstNodeRef, - target: AstNodeRef, - is_async: bool, - ) -> Self { - Self { - target_kind, - iterable, - target, - is_async, - } - } - pub(crate) fn iterable(&self) -> &ast::Expr { self.iterable.node() } @@ -1031,6 +1018,18 @@ impl From<&ast::ExprName> for DefinitionNodeKey { } } +impl From<&ast::ExprAttribute> for DefinitionNodeKey { + fn from(node: &ast::ExprAttribute) -> Self { + Self(NodeKey::from_node(node)) + } +} + +impl From<&ast::ExprSubscript> for DefinitionNodeKey { + fn from(node: &ast::ExprSubscript) -> Self { + Self(NodeKey::from_node(node)) + } +} + impl From<&ast::ExprNamed> for DefinitionNodeKey { fn from(node: &ast::ExprNamed) -> Self { Self(NodeKey::from_node(node)) diff --git a/crates/ty_python_semantic/src/semantic_index/expression.rs b/crates/ty_python_semantic/src/semantic_index/expression.rs index f3afd45f497e6..1c5178b244a87 100644 --- a/crates/ty_python_semantic/src/semantic_index/expression.rs +++ b/crates/ty_python_semantic/src/semantic_index/expression.rs @@ -1,6 +1,6 @@ use crate::ast_node_ref::AstNodeRef; use crate::db::Db; -use crate::semantic_index::symbol::{FileScopeId, ScopeId}; +use crate::semantic_index::place::{FileScopeId, ScopeId}; use ruff_db::files::File; use ruff_python_ast as ast; use salsa; diff --git a/crates/ty_python_semantic/src/semantic_index/narrowing_constraints.rs b/crates/ty_python_semantic/src/semantic_index/narrowing_constraints.rs index 83bfb0d25dbf9..fa6280ead67ce 100644 --- a/crates/ty_python_semantic/src/semantic_index/narrowing_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/narrowing_constraints.rs @@ -1,7 +1,7 @@ //! # Narrowing constraints //! //! When building a semantic index for a file, we associate each binding with a _narrowing -//! constraint_, which constrains the type of the binding's symbol. Note that a binding can be +//! constraint_, which constrains the type of the binding's place. Note that a binding can be //! associated with a different narrowing constraint at different points in a file. See the //! [`use_def`][crate::semantic_index::use_def] module for more details. //! @@ -34,7 +34,7 @@ use crate::semantic_index::predicate::ScopedPredicateId; /// A narrowing constraint associated with a live binding. /// -/// A constraint is a list of [`Predicate`]s that each constrain the type of the binding's symbol. +/// A constraint is a list of [`Predicate`]s that each constrain the type of the binding's place. /// /// [`Predicate`]: crate::semantic_index::predicate::Predicate pub(crate) type ScopedNarrowingConstraint = List; @@ -46,7 +46,7 @@ pub(crate) enum ConstraintKey { } /// One of the [`Predicate`]s in a narrowing constraint, which constraints the type of the -/// binding's symbol. +/// binding's place. /// /// Note that those [`Predicate`]s are stored in [their own per-scope /// arena][crate::semantic_index::predicate::Predicates], so internally we use a diff --git a/crates/ty_python_semantic/src/semantic_index/place.rs b/crates/ty_python_semantic/src/semantic_index/place.rs new file mode 100644 index 0000000000000..4862c61b2ad16 --- /dev/null +++ b/crates/ty_python_semantic/src/semantic_index/place.rs @@ -0,0 +1,942 @@ +use std::convert::Infallible; +use std::hash::{Hash, Hasher}; +use std::ops::Range; + +use bitflags::bitflags; +use hashbrown::hash_map::RawEntryMut; +use ruff_db::files::File; +use ruff_db::parsed::ParsedModule; +use ruff_index::{IndexVec, newtype_index}; +use ruff_python_ast as ast; +use ruff_python_ast::name::Name; +use rustc_hash::FxHasher; +use smallvec::{SmallVec, smallvec}; + +use crate::Db; +use crate::ast_node_ref::AstNodeRef; +use crate::node_key::NodeKey; +use crate::semantic_index::visibility_constraints::ScopedVisibilityConstraintId; +use crate::semantic_index::{PlaceSet, SemanticIndex, semantic_index}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub(crate) enum PlaceExprSubSegment { + /// A member access, e.g. `.y` in `x.y` + Member(ast::name::Name), + /// An integer-based index access, e.g. `[1]` in `x[1]` + IntSubscript(ast::Int), + /// A string-based index access, e.g. `["foo"]` in `x["foo"]` + StringSubscript(String), +} + +impl PlaceExprSubSegment { + pub(crate) fn as_member(&self) -> Option<&ast::name::Name> { + match self { + PlaceExprSubSegment::Member(name) => Some(name), + _ => None, + } + } +} + +/// An expression that can be the target of a `Definition`. +/// If you want to perform a comparison based on the equality of segments (without including +/// flags), use [`PlaceSegments`]. +#[derive(Eq, PartialEq, Debug)] +pub struct PlaceExpr { + root_name: Name, + sub_segments: SmallVec<[PlaceExprSubSegment; 1]>, + flags: PlaceFlags, +} + +impl std::fmt::Display for PlaceExpr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.root_name)?; + for segment in &self.sub_segments { + match segment { + PlaceExprSubSegment::Member(name) => write!(f, ".{name}")?, + PlaceExprSubSegment::IntSubscript(int) => write!(f, "[{int}]")?, + PlaceExprSubSegment::StringSubscript(string) => write!(f, "[\"{string}\"]")?, + } + } + Ok(()) + } +} + +impl TryFrom<&ast::name::Name> for PlaceExpr { + type Error = Infallible; + + fn try_from(name: &ast::name::Name) -> Result { + Ok(PlaceExpr::name(name.clone())) + } +} + +impl TryFrom for PlaceExpr { + type Error = Infallible; + + fn try_from(name: ast::name::Name) -> Result { + Ok(PlaceExpr::name(name)) + } +} + +impl TryFrom<&ast::ExprAttribute> for PlaceExpr { + type Error = (); + + fn try_from(attr: &ast::ExprAttribute) -> Result { + let mut place = PlaceExpr::try_from(&*attr.value)?; + place + .sub_segments + .push(PlaceExprSubSegment::Member(attr.attr.id.clone())); + Ok(place) + } +} + +impl TryFrom for PlaceExpr { + type Error = (); + + fn try_from(attr: ast::ExprAttribute) -> Result { + let mut place = PlaceExpr::try_from(&*attr.value)?; + place + .sub_segments + .push(PlaceExprSubSegment::Member(attr.attr.id)); + Ok(place) + } +} + +impl TryFrom<&ast::ExprSubscript> for PlaceExpr { + type Error = (); + + fn try_from(subscript: &ast::ExprSubscript) -> Result { + let mut place = PlaceExpr::try_from(&*subscript.value)?; + match &*subscript.slice { + ast::Expr::NumberLiteral(ast::ExprNumberLiteral { + value: ast::Number::Int(index), + .. + }) => { + place + .sub_segments + .push(PlaceExprSubSegment::IntSubscript(index.clone())); + } + ast::Expr::StringLiteral(string) => { + place + .sub_segments + .push(PlaceExprSubSegment::StringSubscript( + string.value.to_string(), + )); + } + _ => { + return Err(()); + } + } + Ok(place) + } +} + +impl TryFrom for PlaceExpr { + type Error = (); + + fn try_from(subscript: ast::ExprSubscript) -> Result { + PlaceExpr::try_from(&subscript) + } +} + +impl TryFrom<&ast::Expr> for PlaceExpr { + type Error = (); + + fn try_from(expr: &ast::Expr) -> Result { + match expr { + ast::Expr::Name(name) => Ok(PlaceExpr::name(name.id.clone())), + ast::Expr::Attribute(attr) => PlaceExpr::try_from(attr), + ast::Expr::Subscript(subscript) => PlaceExpr::try_from(subscript), + _ => Err(()), + } + } +} + +impl PlaceExpr { + pub(super) fn name(name: Name) -> Self { + Self { + root_name: name, + sub_segments: smallvec![], + flags: PlaceFlags::empty(), + } + } + + fn insert_flags(&mut self, flags: PlaceFlags) { + self.flags.insert(flags); + } + + pub(super) fn mark_instance_attribute(&mut self) { + self.flags.insert(PlaceFlags::IS_INSTANCE_ATTRIBUTE); + } + + pub(crate) fn root_name(&self) -> &Name { + &self.root_name + } + + pub(crate) fn sub_segments(&self) -> &[PlaceExprSubSegment] { + &self.sub_segments + } + + pub(crate) fn as_name(&self) -> Option<&Name> { + if self.is_name() { + Some(&self.root_name) + } else { + None + } + } + + /// Assumes that the place expression is a name. + #[track_caller] + pub(crate) fn expect_name(&self) -> &Name { + debug_assert_eq!(self.sub_segments.len(), 0); + &self.root_name + } + + /// Does the place expression have the form `self.{name}` (`self` is the first parameter of the method)? + pub(super) fn is_instance_attribute_named(&self, name: &str) -> bool { + self.is_instance_attribute() + && self.sub_segments.len() == 1 + && self.sub_segments[0].as_member().unwrap().as_str() == name + } + + /// Is the place an instance attribute? + pub fn is_instance_attribute(&self) -> bool { + self.flags.contains(PlaceFlags::IS_INSTANCE_ATTRIBUTE) + } + + /// Is the place used in its containing scope? + pub fn is_used(&self) -> bool { + self.flags.contains(PlaceFlags::IS_USED) + } + + /// Is the place defined in its containing scope? + pub fn is_bound(&self) -> bool { + self.flags.contains(PlaceFlags::IS_BOUND) + } + + /// Is the place declared in its containing scope? + pub fn is_declared(&self) -> bool { + self.flags.contains(PlaceFlags::IS_DECLARED) + } + + /// Is the place just a name? + pub fn is_name(&self) -> bool { + self.sub_segments.is_empty() + } + + pub fn is_name_and(&self, f: impl FnOnce(&str) -> bool) -> bool { + self.is_name() && f(&self.root_name) + } + + /// Does the place expression have the form `.member`? + pub fn is_member(&self) -> bool { + self.sub_segments + .last() + .is_some_and(|last| last.as_member().is_some()) + } + + pub(crate) fn segments(&self) -> PlaceSegments { + PlaceSegments { + root_name: Some(&self.root_name), + sub_segments: &self.sub_segments, + } + } + + // TODO: Ideally this would iterate PlaceSegments instead of RootExprs, both to reduce + // allocation and to avoid having both flagged and non-flagged versions of PlaceExprs. + fn root_exprs(&self) -> RootExprs<'_> { + RootExprs { + expr: self, + len: self.sub_segments.len(), + } + } +} + +struct RootExprs<'e> { + expr: &'e PlaceExpr, + len: usize, +} + +impl Iterator for RootExprs<'_> { + type Item = PlaceExpr; + + fn next(&mut self) -> Option { + if self.len == 0 { + return None; + } + self.len -= 1; + Some(PlaceExpr { + root_name: self.expr.root_name.clone(), + sub_segments: self.expr.sub_segments[..self.len].iter().cloned().collect(), + flags: PlaceFlags::empty(), + }) + } +} + +bitflags! { + /// Flags that can be queried to obtain information about a place in a given scope. + /// + /// See the doc-comment at the top of [`super::use_def`] for explanations of what it + /// means for a place to be *bound* as opposed to *declared*. + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + struct PlaceFlags: u8 { + const IS_USED = 1 << 0; + const IS_BOUND = 1 << 1; + const IS_DECLARED = 1 << 2; + /// TODO: This flag is not yet set by anything + const MARKED_GLOBAL = 1 << 3; + /// TODO: This flag is not yet set by anything + const MARKED_NONLOCAL = 1 << 4; + const IS_INSTANCE_ATTRIBUTE = 1 << 5; + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PlaceSegment<'a> { + /// A first segment of a place expression (root name), e.g. `x` in `x.y.z[0]`. + Name(&'a ast::name::Name), + Member(&'a ast::name::Name), + IntSubscript(&'a ast::Int), + StringSubscript(&'a str), +} + +#[derive(Debug, PartialEq, Eq)] +pub struct PlaceSegments<'a> { + root_name: Option<&'a ast::name::Name>, + sub_segments: &'a [PlaceExprSubSegment], +} + +impl<'a> Iterator for PlaceSegments<'a> { + type Item = PlaceSegment<'a>; + + fn next(&mut self) -> Option { + if let Some(name) = self.root_name.take() { + return Some(PlaceSegment::Name(name)); + } + if self.sub_segments.is_empty() { + return None; + } + let segment = &self.sub_segments[0]; + self.sub_segments = &self.sub_segments[1..]; + Some(match segment { + PlaceExprSubSegment::Member(name) => PlaceSegment::Member(name), + PlaceExprSubSegment::IntSubscript(int) => PlaceSegment::IntSubscript(int), + PlaceExprSubSegment::StringSubscript(string) => PlaceSegment::StringSubscript(string), + }) + } +} + +/// ID that uniquely identifies a place in a file. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct FilePlaceId { + scope: FileScopeId, + scoped_place_id: ScopedPlaceId, +} + +impl FilePlaceId { + pub fn scope(self) -> FileScopeId { + self.scope + } + + pub(crate) fn scoped_place_id(self) -> ScopedPlaceId { + self.scoped_place_id + } +} + +impl From for ScopedPlaceId { + fn from(val: FilePlaceId) -> Self { + val.scoped_place_id() + } +} + +/// ID that uniquely identifies a place inside a [`Scope`]. +#[newtype_index] +#[derive(salsa::Update)] +pub struct ScopedPlaceId; + +/// A cross-module identifier of a scope that can be used as a salsa query parameter. +#[salsa::tracked(debug)] +pub struct ScopeId<'db> { + pub file: File, + + pub file_scope_id: FileScopeId, + + count: countme::Count>, +} + +impl<'db> ScopeId<'db> { + pub(crate) fn is_function_like(self, db: &'db dyn Db) -> bool { + self.node(db).scope_kind().is_function_like() + } + + pub(crate) fn is_type_parameter(self, db: &'db dyn Db) -> bool { + self.node(db).scope_kind().is_type_parameter() + } + + pub(crate) fn node(self, db: &dyn Db) -> &NodeWithScopeKind { + self.scope(db).node() + } + + pub(crate) fn scope(self, db: &dyn Db) -> &Scope { + semantic_index(db, self.file(db)).scope(self.file_scope_id(db)) + } + + #[cfg(test)] + pub(crate) fn name(self, db: &'db dyn Db) -> &'db str { + match self.node(db) { + NodeWithScopeKind::Module => "", + NodeWithScopeKind::Class(class) | NodeWithScopeKind::ClassTypeParameters(class) => { + class.name.as_str() + } + NodeWithScopeKind::Function(function) + | NodeWithScopeKind::FunctionTypeParameters(function) => function.name.as_str(), + NodeWithScopeKind::TypeAlias(type_alias) + | NodeWithScopeKind::TypeAliasTypeParameters(type_alias) => type_alias + .name + .as_name_expr() + .map(|name| name.id.as_str()) + .unwrap_or(""), + NodeWithScopeKind::Lambda(_) => "", + NodeWithScopeKind::ListComprehension(_) => "", + NodeWithScopeKind::SetComprehension(_) => "", + NodeWithScopeKind::DictComprehension(_) => "", + NodeWithScopeKind::GeneratorExpression(_) => "", + } + } +} + +/// ID that uniquely identifies a scope inside of a module. +#[newtype_index] +#[derive(salsa::Update)] +pub struct FileScopeId; + +impl FileScopeId { + /// Returns the scope id of the module-global scope. + pub fn global() -> Self { + FileScopeId::from_u32(0) + } + + pub fn is_global(self) -> bool { + self == FileScopeId::global() + } + + pub fn to_scope_id(self, db: &dyn Db, file: File) -> ScopeId<'_> { + let index = semantic_index(db, file); + index.scope_ids_by_scope[self] + } + + pub(crate) fn is_generator_function(self, index: &SemanticIndex) -> bool { + index.generator_functions.contains(&self) + } +} + +#[derive(Debug, salsa::Update)] +pub struct Scope { + parent: Option, + node: NodeWithScopeKind, + descendants: Range, + reachability: ScopedVisibilityConstraintId, +} + +impl Scope { + pub(super) fn new( + parent: Option, + node: NodeWithScopeKind, + descendants: Range, + reachability: ScopedVisibilityConstraintId, + ) -> Self { + Scope { + parent, + node, + descendants, + reachability, + } + } + + pub fn parent(&self) -> Option { + self.parent + } + + pub fn node(&self) -> &NodeWithScopeKind { + &self.node + } + + pub fn kind(&self) -> ScopeKind { + self.node().scope_kind() + } + + pub fn descendants(&self) -> Range { + self.descendants.clone() + } + + pub(super) fn extend_descendants(&mut self, children_end: FileScopeId) { + self.descendants = self.descendants.start..children_end; + } + + pub(crate) fn is_eager(&self) -> bool { + self.kind().is_eager() + } + + pub(crate) fn reachability(&self) -> ScopedVisibilityConstraintId { + self.reachability + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ScopeKind { + Module, + Annotation, + Class, + Function, + Lambda, + Comprehension, + TypeAlias, +} + +impl ScopeKind { + pub(crate) fn is_eager(self) -> bool { + match self { + ScopeKind::Module | ScopeKind::Class | ScopeKind::Comprehension => true, + ScopeKind::Annotation + | ScopeKind::Function + | ScopeKind::Lambda + | ScopeKind::TypeAlias => false, + } + } + + pub(crate) fn is_function_like(self) -> bool { + // Type parameter scopes behave like function scopes in terms of name resolution; CPython + // place table also uses the term "function-like" for these scopes. + matches!( + self, + ScopeKind::Annotation + | ScopeKind::Function + | ScopeKind::Lambda + | ScopeKind::TypeAlias + | ScopeKind::Comprehension + ) + } + + pub(crate) fn is_class(self) -> bool { + matches!(self, ScopeKind::Class) + } + + pub(crate) fn is_type_parameter(self) -> bool { + matches!(self, ScopeKind::Annotation | ScopeKind::TypeAlias) + } +} + +/// [`PlaceExpr`] table for a specific [`Scope`]. +#[derive(Default, salsa::Update)] +pub struct PlaceTable { + /// The place expressions in this scope. + places: IndexVec, + + /// The set of places. + place_set: PlaceSet, +} + +impl PlaceTable { + fn shrink_to_fit(&mut self) { + self.places.shrink_to_fit(); + } + + pub(crate) fn place_expr(&self, place_id: impl Into) -> &PlaceExpr { + &self.places[place_id.into()] + } + + /// Iterate over the "root" expressions of the place (e.g. `x.y.z`, `x.y`, `x` for `x.y.z[0]`). + pub(crate) fn root_place_exprs( + &self, + place_expr: &PlaceExpr, + ) -> impl Iterator { + place_expr + .root_exprs() + .filter_map(|place_expr| self.place_by_expr(&place_expr)) + } + + #[expect(unused)] + pub(crate) fn place_ids(&self) -> impl Iterator { + self.places.indices() + } + + pub fn places(&self) -> impl Iterator { + self.places.iter() + } + + pub fn symbols(&self) -> impl Iterator { + self.places().filter(|place_expr| place_expr.is_name()) + } + + pub fn instance_attributes(&self) -> impl Iterator { + self.places() + .filter(|place_expr| place_expr.is_instance_attribute()) + } + + /// Returns the place named `name`. + #[allow(unused)] // used in tests + pub(crate) fn place_by_name(&self, name: &str) -> Option<&PlaceExpr> { + let id = self.place_id_by_name(name)?; + Some(self.place_expr(id)) + } + + /// Returns the flagged place by the unflagged place expression. + /// + /// TODO: Ideally this would take a [`PlaceSegments`] instead of [`PlaceExpr`], to avoid the + /// awkward distinction between "flagged" (canonical) and unflagged [`PlaceExpr`]; in that + /// world, we would only create [`PlaceExpr`] in semantic indexing; in type inference we'd + /// create [`PlaceSegments`] if we need to look up a [`PlaceExpr`]. The [`PlaceTable`] would + /// need to gain the ability to hash and look up by a [`PlaceSegments`]. + pub(crate) fn place_by_expr(&self, place_expr: &PlaceExpr) -> Option<&PlaceExpr> { + let id = self.place_id_by_expr(place_expr)?; + Some(self.place_expr(id)) + } + + /// Returns the [`ScopedPlaceId`] of the place named `name`. + pub(crate) fn place_id_by_name(&self, name: &str) -> Option { + let (id, ()) = self + .place_set + .raw_entry() + .from_hash(Self::hash_name(name), |id| { + self.place_expr(*id).as_name().map(Name::as_str) == Some(name) + })?; + + Some(*id) + } + + /// Returns the [`ScopedPlaceId`] of the place expression. + pub(crate) fn place_id_by_expr(&self, place_expr: &PlaceExpr) -> Option { + let (id, ()) = self + .place_set + .raw_entry() + .from_hash(Self::hash_place_expr(place_expr), |id| { + self.place_expr(*id).segments() == place_expr.segments() + })?; + + Some(*id) + } + + pub(crate) fn place_id_by_instance_attribute_name(&self, name: &str) -> Option { + self.places + .indices() + .find(|id| self.places[*id].is_instance_attribute_named(name)) + } + + fn hash_name(name: &str) -> u64 { + let mut hasher = FxHasher::default(); + name.hash(&mut hasher); + hasher.finish() + } + + fn hash_place_expr(place_expr: &PlaceExpr) -> u64 { + let mut hasher = FxHasher::default(); + place_expr.root_name().as_str().hash(&mut hasher); + for segment in &place_expr.sub_segments { + match segment { + PlaceExprSubSegment::Member(name) => name.hash(&mut hasher), + PlaceExprSubSegment::IntSubscript(int) => int.hash(&mut hasher), + PlaceExprSubSegment::StringSubscript(string) => string.hash(&mut hasher), + } + } + hasher.finish() + } +} + +impl PartialEq for PlaceTable { + fn eq(&self, other: &Self) -> bool { + // We don't need to compare the place_set because the place is already captured in `PlaceExpr`. + self.places == other.places + } +} + +impl Eq for PlaceTable {} + +impl std::fmt::Debug for PlaceTable { + /// Exclude the `place_set` field from the debug output. + /// It's very noisy and not useful for debugging. + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("PlaceTable") + .field(&self.places) + .finish_non_exhaustive() + } +} + +#[derive(Debug, Default)] +pub(super) struct PlaceTableBuilder { + table: PlaceTable, + + associated_place_ids: IndexVec>, +} + +impl PlaceTableBuilder { + pub(super) fn add_symbol(&mut self, name: Name) -> (ScopedPlaceId, bool) { + let hash = PlaceTable::hash_name(&name); + let entry = self + .table + .place_set + .raw_entry_mut() + .from_hash(hash, |id| self.table.places[*id].as_name() == Some(&name)); + + match entry { + RawEntryMut::Occupied(entry) => (*entry.key(), false), + RawEntryMut::Vacant(entry) => { + let symbol = PlaceExpr::name(name); + + let id = self.table.places.push(symbol); + entry.insert_with_hasher(hash, id, (), |id| { + PlaceTable::hash_place_expr(&self.table.places[*id]) + }); + let new_id = self.associated_place_ids.push(vec![]); + debug_assert_eq!(new_id, id); + (id, true) + } + } + } + + pub(super) fn add_place(&mut self, place_expr: PlaceExpr) -> (ScopedPlaceId, bool) { + let hash = PlaceTable::hash_place_expr(&place_expr); + let entry = self.table.place_set.raw_entry_mut().from_hash(hash, |id| { + self.table.places[*id].segments() == place_expr.segments() + }); + + match entry { + RawEntryMut::Occupied(entry) => (*entry.key(), false), + RawEntryMut::Vacant(entry) => { + let id = self.table.places.push(place_expr); + entry.insert_with_hasher(hash, id, (), |id| { + PlaceTable::hash_place_expr(&self.table.places[*id]) + }); + let new_id = self.associated_place_ids.push(vec![]); + debug_assert_eq!(new_id, id); + for root in self.table.places[id].root_exprs() { + if let Some(root_id) = self.table.place_id_by_expr(&root) { + self.associated_place_ids[root_id].push(id); + } + } + (id, true) + } + } + } + + pub(super) fn mark_place_bound(&mut self, id: ScopedPlaceId) { + self.table.places[id].insert_flags(PlaceFlags::IS_BOUND); + } + + pub(super) fn mark_place_declared(&mut self, id: ScopedPlaceId) { + self.table.places[id].insert_flags(PlaceFlags::IS_DECLARED); + } + + pub(super) fn mark_place_used(&mut self, id: ScopedPlaceId) { + self.table.places[id].insert_flags(PlaceFlags::IS_USED); + } + + pub(super) fn places(&self) -> impl Iterator { + self.table.places() + } + + pub(super) fn place_id_by_expr(&self, place_expr: &PlaceExpr) -> Option { + self.table.place_id_by_expr(place_expr) + } + + pub(super) fn place_expr(&self, place_id: impl Into) -> &PlaceExpr { + self.table.place_expr(place_id) + } + + /// Returns the place IDs associated with the place (e.g. `x.y`, `x.y.z`, `x.y.z[0]` for `x`). + pub(super) fn associated_place_ids( + &self, + place: ScopedPlaceId, + ) -> impl Iterator { + self.associated_place_ids[place].iter().copied() + } + + pub(super) fn finish(mut self) -> PlaceTable { + self.table.shrink_to_fit(); + self.table + } +} + +/// Reference to a node that introduces a new scope. +#[derive(Copy, Clone, Debug)] +pub(crate) enum NodeWithScopeRef<'a> { + Module, + Class(&'a ast::StmtClassDef), + Function(&'a ast::StmtFunctionDef), + Lambda(&'a ast::ExprLambda), + FunctionTypeParameters(&'a ast::StmtFunctionDef), + ClassTypeParameters(&'a ast::StmtClassDef), + TypeAlias(&'a ast::StmtTypeAlias), + TypeAliasTypeParameters(&'a ast::StmtTypeAlias), + ListComprehension(&'a ast::ExprListComp), + SetComprehension(&'a ast::ExprSetComp), + DictComprehension(&'a ast::ExprDictComp), + GeneratorExpression(&'a ast::ExprGenerator), +} + +impl NodeWithScopeRef<'_> { + /// Converts the unowned reference to an owned [`NodeWithScopeKind`]. + /// + /// # Safety + /// The node wrapped by `self` must be a child of `module`. + #[expect(unsafe_code)] + pub(super) unsafe fn to_kind(self, module: ParsedModule) -> NodeWithScopeKind { + unsafe { + match self { + NodeWithScopeRef::Module => NodeWithScopeKind::Module, + NodeWithScopeRef::Class(class) => { + NodeWithScopeKind::Class(AstNodeRef::new(module, class)) + } + NodeWithScopeRef::Function(function) => { + NodeWithScopeKind::Function(AstNodeRef::new(module, function)) + } + NodeWithScopeRef::TypeAlias(type_alias) => { + NodeWithScopeKind::TypeAlias(AstNodeRef::new(module, type_alias)) + } + NodeWithScopeRef::TypeAliasTypeParameters(type_alias) => { + NodeWithScopeKind::TypeAliasTypeParameters(AstNodeRef::new(module, type_alias)) + } + NodeWithScopeRef::Lambda(lambda) => { + NodeWithScopeKind::Lambda(AstNodeRef::new(module, lambda)) + } + NodeWithScopeRef::FunctionTypeParameters(function) => { + NodeWithScopeKind::FunctionTypeParameters(AstNodeRef::new(module, function)) + } + NodeWithScopeRef::ClassTypeParameters(class) => { + NodeWithScopeKind::ClassTypeParameters(AstNodeRef::new(module, class)) + } + NodeWithScopeRef::ListComprehension(comprehension) => { + NodeWithScopeKind::ListComprehension(AstNodeRef::new(module, comprehension)) + } + NodeWithScopeRef::SetComprehension(comprehension) => { + NodeWithScopeKind::SetComprehension(AstNodeRef::new(module, comprehension)) + } + NodeWithScopeRef::DictComprehension(comprehension) => { + NodeWithScopeKind::DictComprehension(AstNodeRef::new(module, comprehension)) + } + NodeWithScopeRef::GeneratorExpression(generator) => { + NodeWithScopeKind::GeneratorExpression(AstNodeRef::new(module, generator)) + } + } + } + } + + pub(crate) fn node_key(self) -> NodeWithScopeKey { + match self { + NodeWithScopeRef::Module => NodeWithScopeKey::Module, + NodeWithScopeRef::Class(class) => NodeWithScopeKey::Class(NodeKey::from_node(class)), + NodeWithScopeRef::Function(function) => { + NodeWithScopeKey::Function(NodeKey::from_node(function)) + } + NodeWithScopeRef::Lambda(lambda) => { + NodeWithScopeKey::Lambda(NodeKey::from_node(lambda)) + } + NodeWithScopeRef::FunctionTypeParameters(function) => { + NodeWithScopeKey::FunctionTypeParameters(NodeKey::from_node(function)) + } + NodeWithScopeRef::ClassTypeParameters(class) => { + NodeWithScopeKey::ClassTypeParameters(NodeKey::from_node(class)) + } + NodeWithScopeRef::TypeAlias(type_alias) => { + NodeWithScopeKey::TypeAlias(NodeKey::from_node(type_alias)) + } + NodeWithScopeRef::TypeAliasTypeParameters(type_alias) => { + NodeWithScopeKey::TypeAliasTypeParameters(NodeKey::from_node(type_alias)) + } + NodeWithScopeRef::ListComprehension(comprehension) => { + NodeWithScopeKey::ListComprehension(NodeKey::from_node(comprehension)) + } + NodeWithScopeRef::SetComprehension(comprehension) => { + NodeWithScopeKey::SetComprehension(NodeKey::from_node(comprehension)) + } + NodeWithScopeRef::DictComprehension(comprehension) => { + NodeWithScopeKey::DictComprehension(NodeKey::from_node(comprehension)) + } + NodeWithScopeRef::GeneratorExpression(generator) => { + NodeWithScopeKey::GeneratorExpression(NodeKey::from_node(generator)) + } + } + } +} + +/// Node that introduces a new scope. +#[derive(Clone, Debug, salsa::Update)] +pub enum NodeWithScopeKind { + Module, + Class(AstNodeRef), + ClassTypeParameters(AstNodeRef), + Function(AstNodeRef), + FunctionTypeParameters(AstNodeRef), + TypeAliasTypeParameters(AstNodeRef), + TypeAlias(AstNodeRef), + Lambda(AstNodeRef), + ListComprehension(AstNodeRef), + SetComprehension(AstNodeRef), + DictComprehension(AstNodeRef), + GeneratorExpression(AstNodeRef), +} + +impl NodeWithScopeKind { + pub(crate) const fn scope_kind(&self) -> ScopeKind { + match self { + Self::Module => ScopeKind::Module, + Self::Class(_) => ScopeKind::Class, + Self::Function(_) => ScopeKind::Function, + Self::Lambda(_) => ScopeKind::Lambda, + Self::FunctionTypeParameters(_) + | Self::ClassTypeParameters(_) + | Self::TypeAliasTypeParameters(_) => ScopeKind::Annotation, + Self::TypeAlias(_) => ScopeKind::TypeAlias, + Self::ListComprehension(_) + | Self::SetComprehension(_) + | Self::DictComprehension(_) + | Self::GeneratorExpression(_) => ScopeKind::Comprehension, + } + } + + pub fn expect_class(&self) -> &ast::StmtClassDef { + match self { + Self::Class(class) => class.node(), + _ => panic!("expected class"), + } + } + + pub(crate) const fn as_class(&self) -> Option<&ast::StmtClassDef> { + match self { + Self::Class(class) => Some(class.node()), + _ => None, + } + } + + pub fn expect_function(&self) -> &ast::StmtFunctionDef { + self.as_function().expect("expected function") + } + + pub fn expect_type_alias(&self) -> &ast::StmtTypeAlias { + match self { + Self::TypeAlias(type_alias) => type_alias.node(), + _ => 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)] +pub(crate) enum NodeWithScopeKey { + Module, + Class(NodeKey), + ClassTypeParameters(NodeKey), + Function(NodeKey), + FunctionTypeParameters(NodeKey), + TypeAlias(NodeKey), + TypeAliasTypeParameters(NodeKey), + Lambda(NodeKey), + ListComprehension(NodeKey), + SetComprehension(NodeKey), + DictComprehension(NodeKey), + GeneratorExpression(NodeKey), +} diff --git a/crates/ty_python_semantic/src/semantic_index/predicate.rs b/crates/ty_python_semantic/src/semantic_index/predicate.rs index 86e3ba903a60e..06b71396e7040 100644 --- a/crates/ty_python_semantic/src/semantic_index/predicate.rs +++ b/crates/ty_python_semantic/src/semantic_index/predicate.rs @@ -14,7 +14,7 @@ use ruff_python_ast::Singleton; use crate::db::Db; use crate::semantic_index::expression::Expression; use crate::semantic_index::global_scope; -use crate::semantic_index::symbol::{FileScopeId, ScopeId, ScopedSymbolId}; +use crate::semantic_index::place::{FileScopeId, ScopeId, ScopedPlaceId}; // A scoped identifier for each `Predicate` in a scope. #[newtype_index] @@ -144,13 +144,13 @@ pub(crate) struct StarImportPlaceholderPredicate<'db> { /// Each symbol imported by a `*` import has a separate predicate associated with it: /// this field identifies which symbol that is. /// - /// Note that a [`ScopedSymbolId`] is only meaningful if you also know the scope + /// Note that a [`ScopedPlaceId`] is only meaningful if you also know the scope /// it is relative to. For this specific struct, however, there's no need to store a /// separate field to hold the ID of the scope. `StarImportPredicate`s are only created /// for valid `*`-import definitions, and valid `*`-import definitions can only ever /// exist in the global scope; thus, we know that the `symbol_id` here will be relative /// to the global scope of the importing file. - pub(crate) symbol_id: ScopedSymbolId, + pub(crate) symbol_id: ScopedPlaceId, pub(crate) referenced_file: File, } diff --git a/crates/ty_python_semantic/src/semantic_index/symbol.rs b/crates/ty_python_semantic/src/semantic_index/symbol.rs deleted file mode 100644 index 4d25978bed26a..0000000000000 --- a/crates/ty_python_semantic/src/semantic_index/symbol.rs +++ /dev/null @@ -1,589 +0,0 @@ -use std::hash::{Hash, Hasher}; -use std::ops::Range; - -use bitflags::bitflags; -use hashbrown::hash_map::RawEntryMut; -use ruff_db::files::File; -use ruff_db::parsed::ParsedModule; -use ruff_index::{IndexVec, newtype_index}; -use ruff_python_ast as ast; -use ruff_python_ast::name::Name; -use rustc_hash::FxHasher; - -use crate::Db; -use crate::ast_node_ref::AstNodeRef; -use crate::node_key::NodeKey; -use crate::semantic_index::visibility_constraints::ScopedVisibilityConstraintId; -use crate::semantic_index::{SemanticIndex, SymbolMap, semantic_index}; - -#[derive(Eq, PartialEq, Debug)] -pub struct Symbol { - name: Name, - flags: SymbolFlags, -} - -impl Symbol { - fn new(name: Name) -> Self { - Self { - name, - flags: SymbolFlags::empty(), - } - } - - fn insert_flags(&mut self, flags: SymbolFlags) { - self.flags.insert(flags); - } - - /// The symbol's name. - pub fn name(&self) -> &Name { - &self.name - } - - /// Is the symbol used in its containing scope? - pub fn is_used(&self) -> bool { - self.flags.contains(SymbolFlags::IS_USED) - } - - /// Is the symbol defined in its containing scope? - pub fn is_bound(&self) -> bool { - self.flags.contains(SymbolFlags::IS_BOUND) - } - - /// Is the symbol declared in its containing scope? - pub fn is_declared(&self) -> bool { - self.flags.contains(SymbolFlags::IS_DECLARED) - } -} - -bitflags! { - /// Flags that can be queried to obtain information about a symbol in a given scope. - /// - /// See the doc-comment at the top of [`super::use_def`] for explanations of what it - /// means for a symbol to be *bound* as opposed to *declared*. - #[derive(Copy, Clone, Debug, Eq, PartialEq)] - struct SymbolFlags: u8 { - const IS_USED = 1 << 0; - const IS_BOUND = 1 << 1; - const IS_DECLARED = 1 << 2; - /// TODO: This flag is not yet set by anything - const MARKED_GLOBAL = 1 << 3; - /// TODO: This flag is not yet set by anything - const MARKED_NONLOCAL = 1 << 4; - } -} - -/// ID that uniquely identifies a symbol in a file. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] -pub struct FileSymbolId { - scope: FileScopeId, - scoped_symbol_id: ScopedSymbolId, -} - -impl FileSymbolId { - pub fn scope(self) -> FileScopeId { - self.scope - } - - pub(crate) fn scoped_symbol_id(self) -> ScopedSymbolId { - self.scoped_symbol_id - } -} - -impl From for ScopedSymbolId { - fn from(val: FileSymbolId) -> Self { - val.scoped_symbol_id() - } -} - -/// Symbol ID that uniquely identifies a symbol inside a [`Scope`]. -#[newtype_index] -#[derive(salsa::Update)] -pub struct ScopedSymbolId; - -/// A cross-module identifier of a scope that can be used as a salsa query parameter. -#[salsa::tracked(debug)] -pub struct ScopeId<'db> { - pub file: File, - - pub file_scope_id: FileScopeId, - - count: countme::Count>, -} - -impl<'db> ScopeId<'db> { - pub(crate) fn is_function_like(self, db: &'db dyn Db) -> bool { - self.node(db).scope_kind().is_function_like() - } - - pub(crate) fn is_type_parameter(self, db: &'db dyn Db) -> bool { - self.node(db).scope_kind().is_type_parameter() - } - - pub(crate) fn node(self, db: &dyn Db) -> &NodeWithScopeKind { - self.scope(db).node() - } - - pub(crate) fn scope(self, db: &dyn Db) -> &Scope { - semantic_index(db, self.file(db)).scope(self.file_scope_id(db)) - } - - #[cfg(test)] - pub(crate) fn name(self, db: &'db dyn Db) -> &'db str { - match self.node(db) { - NodeWithScopeKind::Module => "", - NodeWithScopeKind::Class(class) | NodeWithScopeKind::ClassTypeParameters(class) => { - class.name.as_str() - } - NodeWithScopeKind::Function(function) - | NodeWithScopeKind::FunctionTypeParameters(function) => function.name.as_str(), - NodeWithScopeKind::TypeAlias(type_alias) - | NodeWithScopeKind::TypeAliasTypeParameters(type_alias) => type_alias - .name - .as_name_expr() - .map(|name| name.id.as_str()) - .unwrap_or(""), - NodeWithScopeKind::Lambda(_) => "", - NodeWithScopeKind::ListComprehension(_) => "", - NodeWithScopeKind::SetComprehension(_) => "", - NodeWithScopeKind::DictComprehension(_) => "", - NodeWithScopeKind::GeneratorExpression(_) => "", - } - } -} - -/// ID that uniquely identifies a scope inside of a module. -#[newtype_index] -#[derive(salsa::Update)] -pub struct FileScopeId; - -impl FileScopeId { - /// Returns the scope id of the module-global scope. - pub fn global() -> Self { - FileScopeId::from_u32(0) - } - - pub fn is_global(self) -> bool { - self == FileScopeId::global() - } - - pub fn to_scope_id(self, db: &dyn Db, file: File) -> ScopeId<'_> { - let index = semantic_index(db, file); - index.scope_ids_by_scope[self] - } - - pub(crate) fn is_generator_function(self, index: &SemanticIndex) -> bool { - index.generator_functions.contains(&self) - } -} - -#[derive(Debug, salsa::Update)] -pub struct Scope { - parent: Option, - node: NodeWithScopeKind, - descendants: Range, - reachability: ScopedVisibilityConstraintId, -} - -impl Scope { - pub(super) fn new( - parent: Option, - node: NodeWithScopeKind, - descendants: Range, - reachability: ScopedVisibilityConstraintId, - ) -> Self { - Scope { - parent, - node, - descendants, - reachability, - } - } - - pub fn parent(&self) -> Option { - self.parent - } - - pub fn node(&self) -> &NodeWithScopeKind { - &self.node - } - - pub fn kind(&self) -> ScopeKind { - self.node().scope_kind() - } - - pub fn descendants(&self) -> Range { - self.descendants.clone() - } - - pub(super) fn extend_descendants(&mut self, children_end: FileScopeId) { - self.descendants = self.descendants.start..children_end; - } - - pub(crate) fn is_eager(&self) -> bool { - self.kind().is_eager() - } - - pub(crate) fn reachability(&self) -> ScopedVisibilityConstraintId { - self.reachability - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum ScopeKind { - Module, - Annotation, - Class, - Function, - Lambda, - Comprehension, - TypeAlias, -} - -impl ScopeKind { - pub(crate) fn is_eager(self) -> bool { - match self { - ScopeKind::Module | ScopeKind::Class | ScopeKind::Comprehension => true, - ScopeKind::Annotation - | ScopeKind::Function - | ScopeKind::Lambda - | ScopeKind::TypeAlias => false, - } - } - - pub(crate) fn is_function_like(self) -> bool { - // Type parameter scopes behave like function scopes in terms of name resolution; CPython - // symbol table also uses the term "function-like" for these scopes. - matches!( - self, - ScopeKind::Annotation - | ScopeKind::Function - | ScopeKind::Lambda - | ScopeKind::TypeAlias - | ScopeKind::Comprehension - ) - } - - pub(crate) fn is_class(self) -> bool { - matches!(self, ScopeKind::Class) - } - - pub(crate) fn is_type_parameter(self) -> bool { - matches!(self, ScopeKind::Annotation | ScopeKind::TypeAlias) - } -} - -/// Symbol table for a specific [`Scope`]. -#[derive(Default, salsa::Update)] -pub struct SymbolTable { - /// The symbols in this scope. - symbols: IndexVec, - - /// The symbols indexed by name. - symbols_by_name: SymbolMap, -} - -impl SymbolTable { - fn shrink_to_fit(&mut self) { - self.symbols.shrink_to_fit(); - } - - pub(crate) fn symbol(&self, symbol_id: impl Into) -> &Symbol { - &self.symbols[symbol_id.into()] - } - - #[expect(unused)] - pub(crate) fn symbol_ids(&self) -> impl Iterator { - self.symbols.indices() - } - - pub fn symbols(&self) -> impl Iterator { - self.symbols.iter() - } - - /// Returns the symbol named `name`. - pub(crate) fn symbol_by_name(&self, name: &str) -> Option<&Symbol> { - let id = self.symbol_id_by_name(name)?; - Some(self.symbol(id)) - } - - /// Returns the [`ScopedSymbolId`] of the symbol named `name`. - pub(crate) fn symbol_id_by_name(&self, name: &str) -> Option { - let (id, ()) = self - .symbols_by_name - .raw_entry() - .from_hash(Self::hash_name(name), |id| { - self.symbol(*id).name().as_str() == name - })?; - - Some(*id) - } - - fn hash_name(name: &str) -> u64 { - let mut hasher = FxHasher::default(); - name.hash(&mut hasher); - hasher.finish() - } -} - -impl PartialEq for SymbolTable { - fn eq(&self, other: &Self) -> bool { - // We don't need to compare the symbols_by_name because the name is already captured in `Symbol`. - self.symbols == other.symbols - } -} - -impl Eq for SymbolTable {} - -impl std::fmt::Debug for SymbolTable { - /// Exclude the `symbols_by_name` field from the debug output. - /// It's very noisy and not useful for debugging. - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("SymbolTable") - .field(&self.symbols) - .finish_non_exhaustive() - } -} - -#[derive(Debug, Default)] -pub(super) struct SymbolTableBuilder { - table: SymbolTable, -} - -impl SymbolTableBuilder { - pub(super) fn add_symbol(&mut self, name: Name) -> (ScopedSymbolId, bool) { - let hash = SymbolTable::hash_name(&name); - let entry = self - .table - .symbols_by_name - .raw_entry_mut() - .from_hash(hash, |id| self.table.symbols[*id].name() == &name); - - match entry { - RawEntryMut::Occupied(entry) => (*entry.key(), false), - RawEntryMut::Vacant(entry) => { - let symbol = Symbol::new(name); - - let id = self.table.symbols.push(symbol); - entry.insert_with_hasher(hash, id, (), |id| { - SymbolTable::hash_name(self.table.symbols[*id].name().as_str()) - }); - (id, true) - } - } - } - - pub(super) fn mark_symbol_bound(&mut self, id: ScopedSymbolId) { - self.table.symbols[id].insert_flags(SymbolFlags::IS_BOUND); - } - - pub(super) fn mark_symbol_declared(&mut self, id: ScopedSymbolId) { - self.table.symbols[id].insert_flags(SymbolFlags::IS_DECLARED); - } - - pub(super) fn mark_symbol_used(&mut self, id: ScopedSymbolId) { - self.table.symbols[id].insert_flags(SymbolFlags::IS_USED); - } - - pub(super) fn symbols(&self) -> impl Iterator { - self.table.symbols() - } - - pub(super) fn symbol_id_by_name(&self, name: &str) -> Option { - self.table.symbol_id_by_name(name) - } - - pub(super) fn symbol(&self, symbol_id: impl Into) -> &Symbol { - self.table.symbol(symbol_id) - } - - pub(super) fn finish(mut self) -> SymbolTable { - self.table.shrink_to_fit(); - self.table - } -} - -/// Reference to a node that introduces a new scope. -#[derive(Copy, Clone, Debug)] -pub(crate) enum NodeWithScopeRef<'a> { - Module, - Class(&'a ast::StmtClassDef), - Function(&'a ast::StmtFunctionDef), - Lambda(&'a ast::ExprLambda), - FunctionTypeParameters(&'a ast::StmtFunctionDef), - ClassTypeParameters(&'a ast::StmtClassDef), - TypeAlias(&'a ast::StmtTypeAlias), - TypeAliasTypeParameters(&'a ast::StmtTypeAlias), - ListComprehension(&'a ast::ExprListComp), - SetComprehension(&'a ast::ExprSetComp), - DictComprehension(&'a ast::ExprDictComp), - GeneratorExpression(&'a ast::ExprGenerator), -} - -impl NodeWithScopeRef<'_> { - /// Converts the unowned reference to an owned [`NodeWithScopeKind`]. - /// - /// # Safety - /// The node wrapped by `self` must be a child of `module`. - #[expect(unsafe_code)] - pub(super) unsafe fn to_kind(self, module: ParsedModule) -> NodeWithScopeKind { - unsafe { - match self { - NodeWithScopeRef::Module => NodeWithScopeKind::Module, - NodeWithScopeRef::Class(class) => { - NodeWithScopeKind::Class(AstNodeRef::new(module, class)) - } - NodeWithScopeRef::Function(function) => { - NodeWithScopeKind::Function(AstNodeRef::new(module, function)) - } - NodeWithScopeRef::TypeAlias(type_alias) => { - NodeWithScopeKind::TypeAlias(AstNodeRef::new(module, type_alias)) - } - NodeWithScopeRef::TypeAliasTypeParameters(type_alias) => { - NodeWithScopeKind::TypeAliasTypeParameters(AstNodeRef::new(module, type_alias)) - } - NodeWithScopeRef::Lambda(lambda) => { - NodeWithScopeKind::Lambda(AstNodeRef::new(module, lambda)) - } - NodeWithScopeRef::FunctionTypeParameters(function) => { - NodeWithScopeKind::FunctionTypeParameters(AstNodeRef::new(module, function)) - } - NodeWithScopeRef::ClassTypeParameters(class) => { - NodeWithScopeKind::ClassTypeParameters(AstNodeRef::new(module, class)) - } - NodeWithScopeRef::ListComprehension(comprehension) => { - NodeWithScopeKind::ListComprehension(AstNodeRef::new(module, comprehension)) - } - NodeWithScopeRef::SetComprehension(comprehension) => { - NodeWithScopeKind::SetComprehension(AstNodeRef::new(module, comprehension)) - } - NodeWithScopeRef::DictComprehension(comprehension) => { - NodeWithScopeKind::DictComprehension(AstNodeRef::new(module, comprehension)) - } - NodeWithScopeRef::GeneratorExpression(generator) => { - NodeWithScopeKind::GeneratorExpression(AstNodeRef::new(module, generator)) - } - } - } - } - - pub(crate) fn node_key(self) -> NodeWithScopeKey { - match self { - NodeWithScopeRef::Module => NodeWithScopeKey::Module, - NodeWithScopeRef::Class(class) => NodeWithScopeKey::Class(NodeKey::from_node(class)), - NodeWithScopeRef::Function(function) => { - NodeWithScopeKey::Function(NodeKey::from_node(function)) - } - NodeWithScopeRef::Lambda(lambda) => { - NodeWithScopeKey::Lambda(NodeKey::from_node(lambda)) - } - NodeWithScopeRef::FunctionTypeParameters(function) => { - NodeWithScopeKey::FunctionTypeParameters(NodeKey::from_node(function)) - } - NodeWithScopeRef::ClassTypeParameters(class) => { - NodeWithScopeKey::ClassTypeParameters(NodeKey::from_node(class)) - } - NodeWithScopeRef::TypeAlias(type_alias) => { - NodeWithScopeKey::TypeAlias(NodeKey::from_node(type_alias)) - } - NodeWithScopeRef::TypeAliasTypeParameters(type_alias) => { - NodeWithScopeKey::TypeAliasTypeParameters(NodeKey::from_node(type_alias)) - } - NodeWithScopeRef::ListComprehension(comprehension) => { - NodeWithScopeKey::ListComprehension(NodeKey::from_node(comprehension)) - } - NodeWithScopeRef::SetComprehension(comprehension) => { - NodeWithScopeKey::SetComprehension(NodeKey::from_node(comprehension)) - } - NodeWithScopeRef::DictComprehension(comprehension) => { - NodeWithScopeKey::DictComprehension(NodeKey::from_node(comprehension)) - } - NodeWithScopeRef::GeneratorExpression(generator) => { - NodeWithScopeKey::GeneratorExpression(NodeKey::from_node(generator)) - } - } - } -} - -/// Node that introduces a new scope. -#[derive(Clone, Debug, salsa::Update)] -pub enum NodeWithScopeKind { - Module, - Class(AstNodeRef), - ClassTypeParameters(AstNodeRef), - Function(AstNodeRef), - FunctionTypeParameters(AstNodeRef), - TypeAliasTypeParameters(AstNodeRef), - TypeAlias(AstNodeRef), - Lambda(AstNodeRef), - ListComprehension(AstNodeRef), - SetComprehension(AstNodeRef), - DictComprehension(AstNodeRef), - GeneratorExpression(AstNodeRef), -} - -impl NodeWithScopeKind { - pub(crate) const fn scope_kind(&self) -> ScopeKind { - match self { - Self::Module => ScopeKind::Module, - Self::Class(_) => ScopeKind::Class, - Self::Function(_) => ScopeKind::Function, - Self::Lambda(_) => ScopeKind::Lambda, - Self::FunctionTypeParameters(_) - | Self::ClassTypeParameters(_) - | Self::TypeAliasTypeParameters(_) => ScopeKind::Annotation, - Self::TypeAlias(_) => ScopeKind::TypeAlias, - Self::ListComprehension(_) - | Self::SetComprehension(_) - | Self::DictComprehension(_) - | Self::GeneratorExpression(_) => ScopeKind::Comprehension, - } - } - - pub fn expect_class(&self) -> &ast::StmtClassDef { - match self { - Self::Class(class) => class.node(), - _ => panic!("expected class"), - } - } - - pub(crate) const fn as_class(&self) -> Option<&ast::StmtClassDef> { - match self { - Self::Class(class) => Some(class.node()), - _ => None, - } - } - - pub fn expect_function(&self) -> &ast::StmtFunctionDef { - self.as_function().expect("expected function") - } - - pub fn expect_type_alias(&self) -> &ast::StmtTypeAlias { - match self { - Self::TypeAlias(type_alias) => type_alias.node(), - _ => 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)] -pub(crate) enum NodeWithScopeKey { - Module, - Class(NodeKey), - ClassTypeParameters(NodeKey), - Function(NodeKey), - FunctionTypeParameters(NodeKey), - TypeAlias(NodeKey), - TypeAliasTypeParameters(NodeKey), - Lambda(NodeKey), - ListComprehension(NodeKey), - SetComprehension(NodeKey), - DictComprehension(NodeKey), - GeneratorExpression(NodeKey), -} diff --git a/crates/ty_python_semantic/src/semantic_index/use_def.rs b/crates/ty_python_semantic/src/semantic_index/use_def.rs index 7605843db4e20..39647ac7faa16 100644 --- a/crates/ty_python_semantic/src/semantic_index/use_def.rs +++ b/crates/ty_python_semantic/src/semantic_index/use_def.rs @@ -1,12 +1,20 @@ //! First, some terminology: //! -//! * A "binding" gives a new value to a variable. This includes many different Python statements +//! * A "place" is semantically a location where a value can be read or written, and syntactically, +//! an expression that can be the target of an assignment, e.g. `x`, `x[0]`, `x.y`. (The term is +//! borrowed from Rust). In Python syntax, an expression like `f().x` is also allowed as the +//! target so it can be called a place, but we do not record declarations / bindings like `f().x: +//! int`, `f().x = ...`. Type checking itself can be done by recording only assignments to names, +//! but in order to perform type narrowing by attribute/subscript assignments, they must also be +//! recorded. +//! +//! * A "binding" gives a new value to a place. This includes many different Python statements //! (assignment statements of course, but also imports, `def` and `class` statements, `as` //! clauses in `with` and `except` statements, match patterns, and others) and even one //! expression kind (named expressions). It notably does not include annotated assignment -//! statements without a right-hand side value; these do not assign any new value to the -//! variable. We consider function parameters to be bindings as well, since (from the perspective -//! of the function's internal scope), a function parameter begins the scope bound to a value. +//! statements without a right-hand side value; these do not assign any new value to the place. +//! We consider function parameters to be bindings as well, since (from the perspective of the +//! function's internal scope), a function parameter begins the scope bound to a value. //! //! * A "declaration" establishes an upper bound type for the values that a variable may be //! permitted to take on. Annotated assignment statements (with or without an RHS value) are @@ -67,12 +75,12 @@ //! Path(path)`, with the explicit `: Path` annotation, is permitted. //! //! The general rule is that whatever declaration(s) can reach a given binding determine the -//! validity of that binding. If there is a path in which the symbol is not declared, that is a +//! validity of that binding. If there is a path in which the place is not declared, that is a //! declaration of `Unknown`. If multiple declarations can reach a binding, we union them, but by //! default we also issue a type error, since this implicit union of declared types may hide an //! error. //! -//! To support type inference, we build a map from each use of a symbol to the bindings live at +//! To support type inference, we build a map from each use of a place to the bindings live at //! that use, and the type narrowing constraints that apply to each binding. //! //! Let's take this code sample: @@ -103,12 +111,12 @@ //! bindings and infer a type of `Literal[3, 4]` -- the union of `Literal[3]` and `Literal[4]` -- //! for the second use of `x`. //! -//! So that's one question our use-def map needs to answer: given a specific use of a symbol, which +//! So that's one question our use-def map needs to answer: given a specific use of a place, which //! binding(s) can reach that use. In [`AstIds`](crate::semantic_index::ast_ids::AstIds) we number -//! all uses (that means a `Name` node with `Load` context) so we have a `ScopedUseId` to -//! efficiently represent each use. +//! all uses (that means a `Name`/`ExprAttribute`/`ExprSubscript` node with `Load` context) +//! so we have a `ScopedUseId` to efficiently represent each use. //! -//! We also need to know, for a given definition of a symbol, what type narrowing constraints apply +//! We also need to know, for a given definition of a place, what type narrowing constraints apply //! to it. For instance, in this code sample: //! //! ```python @@ -122,70 +130,70 @@ //! can rule out the possibility that `x` is `None` here, which should give us the type //! `Literal[1]` for this use. //! -//! For declared types, we need to be able to answer the question "given a binding to a symbol, -//! which declarations of that symbol can reach the binding?" This allows us to emit a diagnostic +//! For declared types, we need to be able to answer the question "given a binding to a place, +//! which declarations of that place can reach the binding?" This allows us to emit a diagnostic //! if the binding is attempting to bind a value of a type that is not assignable to the declared -//! type for that symbol, at that point in control flow. +//! type for that place, at that point in control flow. //! -//! We also need to know, given a declaration of a symbol, what the inferred type of that symbol is +//! We also need to know, given a declaration of a place, what the inferred type of that place is //! at that point. This allows us to emit a diagnostic in a case like `x = "foo"; x: int`. The //! binding `x = "foo"` occurs before the declaration `x: int`, so according to our //! control-flow-sensitive interpretation of declarations, the assignment is not an error. But the //! declaration is an error, since it would violate the "inferred type must be assignable to //! declared type" rule. //! -//! Another case we need to handle is when a symbol is referenced from a different scope (for -//! example, an import or a nonlocal reference). We call this "public" use of a symbol. For public -//! use of a symbol, we prefer the declared type, if there are any declarations of that symbol; if +//! Another case we need to handle is when a place is referenced from a different scope (for +//! example, an import or a nonlocal reference). We call this "public" use of a place. For public +//! use of a place, we prefer the declared type, if there are any declarations of that place; if //! not, we fall back to the inferred type. So we also need to know which declarations and bindings //! can reach the end of the scope. //! -//! Technically, public use of a symbol could occur from any point in control flow of the scope -//! where the symbol is defined (via inline imports and import cycles, in the case of an import, or -//! via a function call partway through the local scope that ends up using a symbol from the scope +//! Technically, public use of a place could occur from any point in control flow of the scope +//! where the place is defined (via inline imports and import cycles, in the case of an import, or +//! via a function call partway through the local scope that ends up using a place from the scope //! via a global or nonlocal reference.) But modeling this fully accurately requires whole-program -//! analysis that isn't tractable for an efficient analysis, since it means a given symbol could +//! analysis that isn't tractable for an efficient analysis, since it means a given place could //! have a different type every place it's referenced throughout the program, depending on the //! shape of arbitrarily-sized call/import graphs. So we follow other Python type checkers in //! making the simplifying assumption that usually the scope will finish execution before its -//! symbols are made visible to other scopes; for instance, most imports will import from a +//! places are made visible to other scopes; for instance, most imports will import from a //! complete module, not a partially-executed module. (We may want to get a little smarter than //! this in the future for some closures, but for now this is where we start.) //! //! The data structure we build to answer these questions is the `UseDefMap`. It has a -//! `bindings_by_use` vector of [`SymbolBindings`] indexed by [`ScopedUseId`], a -//! `declarations_by_binding` vector of [`SymbolDeclarations`] indexed by [`ScopedDefinitionId`], a -//! `bindings_by_declaration` vector of [`SymbolBindings`] indexed by [`ScopedDefinitionId`], and -//! `public_bindings` and `public_definitions` vectors indexed by [`ScopedSymbolId`]. The values in +//! `bindings_by_use` vector of [`Bindings`] indexed by [`ScopedUseId`], a +//! `declarations_by_binding` vector of [`Declarations`] indexed by [`ScopedDefinitionId`], a +//! `bindings_by_declaration` vector of [`Bindings`] indexed by [`ScopedDefinitionId`], and +//! `public_bindings` and `public_definitions` vectors indexed by [`ScopedPlaceId`]. The values in //! each of these vectors are (in principle) a list of live bindings at that use/definition, or at -//! the end of the scope for that symbol, with a list of the dominating constraints for each +//! the end of the scope for that place, with a list of the dominating constraints for each //! binding. //! //! In order to avoid vectors-of-vectors-of-vectors and all the allocations that would entail, we //! don't actually store these "list of visible definitions" as a vector of [`Definition`]. -//! Instead, [`SymbolBindings`] and [`SymbolDeclarations`] are structs which use bit-sets to track +//! Instead, [`Bindings`] and [`Declarations`] are structs which use bit-sets to track //! definitions (and constraints, in the case of bindings) in terms of [`ScopedDefinitionId`] and //! [`ScopedPredicateId`], which are indices into the `all_definitions` and `predicates` //! indexvecs in the [`UseDefMap`]. //! -//! There is another special kind of possible "definition" for a symbol: there might be a path from -//! the scope entry to a given use in which the symbol is never bound. We model this with a special -//! "unbound" definition (a `None` entry at the start of the `all_definitions` vector). If that -//! sentinel definition is present in the live bindings at a given use, it means that there is a -//! possible path through control flow in which that symbol is unbound. Similarly, if that sentinel -//! is present in the live declarations, it means that the symbol is (possibly) undeclared. +//! There is another special kind of possible "definition" for a place: there might be a path from +//! the scope entry to a given use in which the place is never bound. We model this with a special +//! "unbound/undeclared" definition (a [`DefinitionState::Undefined`] entry at the start of the +//! `all_definitions` vector). If that sentinel definition is present in the live bindings at a +//! given use, it means that there is a possible path through control flow in which that place is +//! unbound. Similarly, if that sentinel is present in the live declarations, it means that the +//! place is (possibly) undeclared. //! //! To build a [`UseDefMap`], the [`UseDefMapBuilder`] is notified of each new use, definition, and //! constraint as they are encountered by the //! [`SemanticIndexBuilder`](crate::semantic_index::builder::SemanticIndexBuilder) AST visit. For -//! each symbol, the builder tracks the `SymbolState` (`SymbolBindings` and `SymbolDeclarations`) -//! for that symbol. When we hit a use or definition of a symbol, we record the necessary parts of -//! the current state for that symbol that we need for that use or definition. When we reach the -//! end of the scope, it records the state for each symbol as the public definitions of that -//! symbol. +//! each place, the builder tracks the `PlaceState` (`Bindings` and `Declarations`) for that place. +//! When we hit a use or definition of a place, we record the necessary parts of the current state +//! for that place that we need for that use or definition. When we reach the end of the scope, it +//! records the state for each place as the public definitions of that place. //! //! Let's walk through the above example. Initially we do not have any record of `x`. When we add -//! the new symbol (before we process the first binding), we create a new undefined `SymbolState` +//! the new place (before we process the first binding), we create a new undefined `PlaceState` //! which has a single live binding (the "unbound" definition) and a single live declaration (the //! "undeclared" definition). When we see `x = 1`, we record that as the sole live binding of `x`. //! The "unbound" binding is no longer visible. Then we see `x = 2`, and we replace `x = 1` as the @@ -193,11 +201,11 @@ //! of `x` are just the `x = 2` definition. //! //! Then we hit the `if` branch. We visit the `test` node (`flag` in this case), since that will -//! happen regardless. Then we take a pre-branch snapshot of the current state for all symbols, +//! happen regardless. Then we take a pre-branch snapshot of the current state for all places, //! which we'll need later. Then we record `flag` as a possible constraint on the current binding //! (`x = 2`), and go ahead and visit the `if` body. When we see `x = 3`, it replaces `x = 2` //! (constrained by `flag`) as the sole live binding of `x`. At the end of the `if` body, we take -//! another snapshot of the current symbol state; we'll call this the post-if-body snapshot. +//! another snapshot of the current place state; we'll call this the post-if-body snapshot. //! //! Now we need to visit the `else` clause. The conditions when entering the `else` clause should //! be the pre-if conditions; if we are entering the `else` clause, we know that the `if` test @@ -247,7 +255,7 @@ //! `__bool__` method of `test` returns type `bool`, we can see both bindings. //! //! Note that we also record visibility constraints for the start of the scope. This is important -//! to determine if a symbol is definitely bound, possibly unbound, or definitely unbound. In the +//! to determine if a place is definitely bound, possibly unbound, or definitely unbound. In the //! example above, The `y = ` binding is constrained by `~test`, so `y` would only be //! definitely-bound if `test` is always truthy. //! @@ -259,34 +267,34 @@ use ruff_index::{IndexVec, newtype_index}; use rustc_hash::FxHashMap; -use self::symbol_state::{ - EagerSnapshot, LiveBindingsIterator, LiveDeclaration, LiveDeclarationsIterator, - ScopedDefinitionId, SymbolBindings, SymbolDeclarations, SymbolState, +use self::place_state::{ + Bindings, Declarations, EagerSnapshot, LiveBindingsIterator, LiveDeclaration, + LiveDeclarationsIterator, PlaceState, ScopedDefinitionId, }; use crate::node_key::NodeKey; use crate::semantic_index::EagerSnapshotResult; use crate::semantic_index::ast_ids::ScopedUseId; -use crate::semantic_index::definition::Definition; +use crate::semantic_index::definition::{Definition, DefinitionState}; use crate::semantic_index::narrowing_constraints::{ ConstraintKey, NarrowingConstraints, NarrowingConstraintsBuilder, NarrowingConstraintsIterator, }; +use crate::semantic_index::place::{FileScopeId, PlaceExpr, ScopeKind, ScopedPlaceId}; use crate::semantic_index::predicate::{ Predicate, Predicates, PredicatesBuilder, ScopedPredicateId, StarImportPlaceholderPredicate, }; -use crate::semantic_index::symbol::{FileScopeId, ScopeKind, ScopedSymbolId}; use crate::semantic_index::visibility_constraints::{ ScopedVisibilityConstraintId, VisibilityConstraints, VisibilityConstraintsBuilder, }; use crate::types::{IntersectionBuilder, Truthiness, Type, infer_narrowing_constraint}; -mod symbol_state; +mod place_state; /// Applicable definitions and constraints for every use of a name. #[derive(Debug, PartialEq, Eq, salsa::Update)] pub(crate) struct UseDefMap<'db> { - /// Array of [`Definition`] in this scope. Only the first entry should be `None`; - /// this represents the implicit "unbound"/"undeclared" definition of every symbol. - all_definitions: IndexVec>>, + /// Array of [`Definition`] in this scope. Only the first entry should be [`DefinitionState::Undefined`]; + /// this represents the implicit "unbound"/"undeclared" definition of every place. + all_definitions: IndexVec>, /// Array of predicates in this scope. predicates: Predicates<'db>, @@ -297,34 +305,31 @@ pub(crate) struct UseDefMap<'db> { /// Array of visibility constraints in this scope. visibility_constraints: VisibilityConstraints, - /// [`SymbolBindings`] reaching a [`ScopedUseId`]. - bindings_by_use: IndexVec, + /// [`Bindings`] reaching a [`ScopedUseId`]. + bindings_by_use: IndexVec, /// Tracks whether or not a given AST node is reachable from the start of the scope. node_reachability: FxHashMap, /// If the definition is a binding (only) -- `x = 1` for example -- then we need - /// [`SymbolDeclarations`] to know whether this binding is permitted by the live declarations. + /// [`Declarations`] to know whether this binding is permitted by the live declarations. /// /// If the definition is both a declaration and a binding -- `x: int = 1` for example -- then /// we don't actually need anything here, all we'll need to validate is that our own RHS is a /// valid assignment to our own annotation. - declarations_by_binding: FxHashMap, SymbolDeclarations>, + declarations_by_binding: FxHashMap, Declarations>, /// If the definition is a declaration (only) -- `x: int` for example -- then we need - /// [`SymbolBindings`] to know whether this declaration is consistent with the previously + /// [`Bindings`] to know whether this declaration is consistent with the previously /// inferred type. /// /// If the definition is both a declaration and a binding -- `x: int = 1` for example -- then /// we don't actually need anything here, all we'll need to validate is that our own RHS is a /// valid assignment to our own annotation. - bindings_by_declaration: FxHashMap, SymbolBindings>, - - /// [`SymbolState`] visible at end of scope for each symbol. - public_symbols: IndexVec, + bindings_by_declaration: FxHashMap, Bindings>, - /// [`SymbolState`] for each instance attribute. - instance_attributes: IndexVec, + /// [`PlaceState`] visible at end of scope for each place. + public_places: IndexVec, /// Snapshot of bindings in this scope that can be used to resolve a reference in a nested /// eager scope. @@ -402,16 +407,9 @@ impl<'db> UseDefMap<'db> { pub(crate) fn public_bindings( &self, - symbol: ScopedSymbolId, - ) -> BindingWithConstraintsIterator<'_, 'db> { - self.bindings_iterator(self.public_symbols[symbol].bindings()) - } - - pub(crate) fn instance_attribute_bindings( - &self, - symbol: ScopedSymbolId, + place: ScopedPlaceId, ) -> BindingWithConstraintsIterator<'_, 'db> { - self.bindings_iterator(self.instance_attributes[symbol].bindings()) + self.bindings_iterator(self.public_places[place].bindings()) } pub(crate) fn eager_snapshot( @@ -422,8 +420,8 @@ impl<'db> UseDefMap<'db> { Some(EagerSnapshot::Constraint(constraint)) => { EagerSnapshotResult::FoundConstraint(*constraint) } - Some(EagerSnapshot::Bindings(symbol_bindings)) => { - EagerSnapshotResult::FoundBindings(self.bindings_iterator(symbol_bindings)) + Some(EagerSnapshot::Bindings(bindings)) => { + EagerSnapshotResult::FoundBindings(self.bindings_iterator(bindings)) } None => EagerSnapshotResult::NotFound, } @@ -445,27 +443,27 @@ impl<'db> UseDefMap<'db> { pub(crate) fn public_declarations<'map>( &'map self, - symbol: ScopedSymbolId, + place: ScopedPlaceId, ) -> DeclarationsIterator<'map, 'db> { - let declarations = self.public_symbols[symbol].declarations(); + let declarations = self.public_places[place].declarations(); self.declarations_iterator(declarations) } pub(crate) fn all_public_declarations<'map>( &'map self, - ) -> impl Iterator)> + 'map { - (0..self.public_symbols.len()) - .map(ScopedSymbolId::from_usize) - .map(|symbol_id| (symbol_id, self.public_declarations(symbol_id))) + ) -> impl Iterator)> + 'map { + (0..self.public_places.len()) + .map(ScopedPlaceId::from_usize) + .map(|place_id| (place_id, self.public_declarations(place_id))) } pub(crate) fn all_public_bindings<'map>( &'map self, - ) -> impl Iterator)> + 'map + ) -> impl Iterator)> + 'map { - (0..self.public_symbols.len()) - .map(ScopedSymbolId::from_usize) - .map(|symbol_id| (symbol_id, self.public_bindings(symbol_id))) + (0..self.public_places.len()) + .map(ScopedPlaceId::from_usize) + .map(|place_id| (place_id, self.public_bindings(place_id))) } /// This function is intended to be called only once inside `TypeInferenceBuilder::infer_function_body`. @@ -487,7 +485,7 @@ impl<'db> UseDefMap<'db> { fn bindings_iterator<'map>( &'map self, - bindings: &'map SymbolBindings, + bindings: &'map Bindings, ) -> BindingWithConstraintsIterator<'map, 'db> { BindingWithConstraintsIterator { all_definitions: &self.all_definitions, @@ -500,7 +498,7 @@ impl<'db> UseDefMap<'db> { fn declarations_iterator<'map>( &'map self, - declarations: &'map SymbolDeclarations, + declarations: &'map Declarations, ) -> DeclarationsIterator<'map, 'db> { DeclarationsIterator { all_definitions: &self.all_definitions, @@ -511,12 +509,12 @@ impl<'db> UseDefMap<'db> { } } -/// Uniquely identifies a snapshot of a symbol state that can be used to resolve a reference in a +/// Uniquely identifies a snapshot of a place state that can be used to resolve a reference in a /// nested eager scope. /// /// An eager scope has its entire body executed immediately at the location where it is defined. /// For any free references in the nested scope, we use the bindings that are visible at the point -/// where the nested scope is defined, instead of using the public type of the symbol. +/// where the nested scope is defined, instead of using the public type of the place. /// /// There is a unique ID for each distinct [`EagerSnapshotKey`] in the file. #[newtype_index] @@ -526,18 +524,18 @@ pub(crate) struct ScopedEagerSnapshotId; pub(crate) struct EagerSnapshotKey { /// The enclosing scope containing the bindings pub(crate) enclosing_scope: FileScopeId, - /// The referenced symbol (in the enclosing scope) - pub(crate) enclosing_symbol: ScopedSymbolId, + /// The referenced place (in the enclosing scope) + pub(crate) enclosing_place: ScopedPlaceId, /// The nested eager scope containing the reference pub(crate) nested_scope: FileScopeId, } -/// A snapshot of symbol states that can be used to resolve a reference in a nested eager scope. +/// A snapshot of place states that can be used to resolve a reference in a nested eager scope. type EagerSnapshots = IndexVec; #[derive(Debug)] pub(crate) struct BindingWithConstraintsIterator<'map, 'db> { - all_definitions: &'map IndexVec>>, + all_definitions: &'map IndexVec>, pub(crate) predicates: &'map Predicates<'db>, pub(crate) narrowing_constraints: &'map NarrowingConstraints, pub(crate) visibility_constraints: &'map VisibilityConstraints, @@ -568,7 +566,7 @@ impl<'map, 'db> Iterator for BindingWithConstraintsIterator<'map, 'db> { impl std::iter::FusedIterator for BindingWithConstraintsIterator<'_, '_> {} pub(crate) struct BindingWithConstraints<'map, 'db> { - pub(crate) binding: Option>, + pub(crate) binding: DefinitionState<'db>, pub(crate) narrowing_constraint: ConstraintsIterator<'map, 'db>, pub(crate) visibility_constraint: ScopedVisibilityConstraintId, } @@ -595,10 +593,10 @@ impl<'db> ConstraintsIterator<'_, 'db> { self, db: &'db dyn crate::Db, base_ty: Type<'db>, - symbol: ScopedSymbolId, + place: ScopedPlaceId, ) -> Type<'db> { let constraint_tys: Vec<_> = self - .filter_map(|constraint| infer_narrowing_constraint(db, constraint, symbol)) + .filter_map(|constraint| infer_narrowing_constraint(db, constraint, place)) .collect(); if constraint_tys.is_empty() { @@ -618,14 +616,14 @@ impl<'db> ConstraintsIterator<'_, 'db> { #[derive(Clone)] pub(crate) struct DeclarationsIterator<'map, 'db> { - all_definitions: &'map IndexVec>>, + all_definitions: &'map IndexVec>, pub(crate) predicates: &'map Predicates<'db>, pub(crate) visibility_constraints: &'map VisibilityConstraints, inner: LiveDeclarationsIterator<'map>, } pub(crate) struct DeclarationWithConstraint<'db> { - pub(crate) declaration: Option>, + pub(crate) declaration: DefinitionState<'db>, pub(crate) visibility_constraint: ScopedVisibilityConstraintId, } @@ -652,8 +650,7 @@ impl std::iter::FusedIterator for DeclarationsIterator<'_, '_> {} /// A snapshot of the definitions and constraints state at a particular point in control flow. #[derive(Clone, Debug)] pub(super) struct FlowSnapshot { - symbol_states: IndexVec, - instance_attribute_states: IndexVec, + place_states: IndexVec, scope_start_visibility: ScopedVisibilityConstraintId, reachability: ScopedVisibilityConstraintId, } @@ -661,7 +658,7 @@ pub(super) struct FlowSnapshot { #[derive(Debug)] pub(super) struct UseDefMapBuilder<'db> { /// Append-only array of [`Definition`]. - all_definitions: IndexVec>>, + all_definitions: IndexVec>, /// Builder of predicates. pub(super) predicates: PredicatesBuilder<'db>, @@ -673,7 +670,7 @@ pub(super) struct UseDefMapBuilder<'db> { pub(super) visibility_constraints: VisibilityConstraintsBuilder, /// A constraint which describes the visibility of the unbound/undeclared state, i.e. - /// whether or not a use of a symbol at the current point in control flow would see + /// whether or not a use of a place at the current point in control flow would see /// the fake `x = ` binding at the start of the scope. This is important for /// cases like the following, where we need to hide the implicit unbound binding in /// the "else" branch: @@ -688,7 +685,7 @@ pub(super) struct UseDefMapBuilder<'db> { pub(super) scope_start_visibility: ScopedVisibilityConstraintId, /// Live bindings at each so-far-recorded use. - bindings_by_use: IndexVec, + bindings_by_use: IndexVec, /// Tracks whether or not the scope start is visible at the current point in control flow. /// This is subtly different from `scope_start_visibility`, as we apply these constraints @@ -725,18 +722,15 @@ pub(super) struct UseDefMapBuilder<'db> { node_reachability: FxHashMap, /// Live declarations for each so-far-recorded binding. - declarations_by_binding: FxHashMap, SymbolDeclarations>, + declarations_by_binding: FxHashMap, Declarations>, /// Live bindings for each so-far-recorded declaration. - bindings_by_declaration: FxHashMap, SymbolBindings>, + bindings_by_declaration: FxHashMap, Bindings>, - /// Currently live bindings and declarations for each symbol. - symbol_states: IndexVec, + /// Currently live bindings and declarations for each place. + place_states: IndexVec, - /// Currently live bindings for each instance attribute. - instance_attribute_states: IndexVec, - - /// Snapshots of symbol states in this scope that can be used to resolve a reference in a + /// Snapshots of place states in this scope that can be used to resolve a reference in a /// nested eager scope. eager_snapshots: EagerSnapshots, @@ -747,7 +741,7 @@ pub(super) struct UseDefMapBuilder<'db> { impl<'db> UseDefMapBuilder<'db> { pub(super) fn new(is_class_scope: bool) -> Self { Self { - all_definitions: IndexVec::from_iter([None]), + all_definitions: IndexVec::from_iter([DefinitionState::Undefined]), predicates: PredicatesBuilder::default(), narrowing_constraints: NarrowingConstraintsBuilder::default(), visibility_constraints: VisibilityConstraintsBuilder::default(), @@ -757,9 +751,8 @@ impl<'db> UseDefMapBuilder<'db> { node_reachability: FxHashMap::default(), declarations_by_binding: FxHashMap::default(), bindings_by_declaration: FxHashMap::default(), - symbol_states: IndexVec::new(), + place_states: IndexVec::new(), eager_snapshots: EagerSnapshots::default(), - instance_attribute_states: IndexVec::new(), is_class_scope, } } @@ -768,38 +761,29 @@ impl<'db> UseDefMapBuilder<'db> { self.reachability = ScopedVisibilityConstraintId::ALWAYS_FALSE; } - pub(super) fn add_symbol(&mut self, symbol: ScopedSymbolId) { - let new_symbol = self - .symbol_states - .push(SymbolState::undefined(self.scope_start_visibility)); - debug_assert_eq!(symbol, new_symbol); - } - - pub(super) fn add_attribute(&mut self, symbol: ScopedSymbolId) { - let new_symbol = self - .instance_attribute_states - .push(SymbolState::undefined(self.scope_start_visibility)); - debug_assert_eq!(symbol, new_symbol); + pub(super) fn add_place(&mut self, place: ScopedPlaceId) { + let new_place = self + .place_states + .push(PlaceState::undefined(self.scope_start_visibility)); + debug_assert_eq!(place, new_place); } - pub(super) fn record_binding(&mut self, symbol: ScopedSymbolId, binding: Definition<'db>) { - let def_id = self.all_definitions.push(Some(binding)); - let symbol_state = &mut self.symbol_states[symbol]; - self.declarations_by_binding - .insert(binding, symbol_state.declarations().clone()); - symbol_state.record_binding(def_id, self.scope_start_visibility, self.is_class_scope); - } - - pub(super) fn record_attribute_binding( + pub(super) fn record_binding( &mut self, - symbol: ScopedSymbolId, + place: ScopedPlaceId, binding: Definition<'db>, + is_place_name: bool, ) { - let def_id = self.all_definitions.push(Some(binding)); - let attribute_state = &mut self.instance_attribute_states[symbol]; + let def_id = self.all_definitions.push(DefinitionState::Defined(binding)); + let place_state = &mut self.place_states[place]; self.declarations_by_binding - .insert(binding, attribute_state.declarations().clone()); - attribute_state.record_binding(def_id, self.scope_start_visibility, self.is_class_scope); + .insert(binding, place_state.declarations().clone()); + place_state.record_binding( + def_id, + self.scope_start_visibility, + self.is_class_scope, + is_place_name, + ); } pub(super) fn add_predicate(&mut self, predicate: Predicate<'db>) -> ScopedPredicateId { @@ -808,11 +792,7 @@ impl<'db> UseDefMapBuilder<'db> { pub(super) fn record_narrowing_constraint(&mut self, predicate: ScopedPredicateId) { let narrowing_constraint = predicate.into(); - for state in &mut self.symbol_states { - state - .record_narrowing_constraint(&mut self.narrowing_constraints, narrowing_constraint); - } - for state in &mut self.instance_attribute_states { + for state in &mut self.place_states { state .record_narrowing_constraint(&mut self.narrowing_constraints, narrowing_constraint); } @@ -822,10 +802,7 @@ impl<'db> UseDefMapBuilder<'db> { &mut self, constraint: ScopedVisibilityConstraintId, ) { - for state in &mut self.symbol_states { - state.record_visibility_constraint(&mut self.visibility_constraints, constraint); - } - for state in &mut self.instance_attribute_states { + for state in &mut self.place_states { state.record_visibility_constraint(&mut self.visibility_constraints, constraint); } self.scope_start_visibility = self @@ -833,13 +810,13 @@ impl<'db> UseDefMapBuilder<'db> { .add_and_constraint(self.scope_start_visibility, constraint); } - /// Snapshot the state of a single symbol at the current point in control flow. + /// Snapshot the state of a single place at the current point in control flow. /// /// This is only used for `*`-import visibility constraints, which are handled differently /// to most other visibility constraints. See the doc-comment for /// [`Self::record_and_negate_star_import_visibility_constraint`] for more details. - pub(super) fn single_symbol_snapshot(&self, symbol: ScopedSymbolId) -> SymbolState { - self.symbol_states[symbol].clone() + pub(super) fn single_place_snapshot(&self, place: ScopedPlaceId) -> PlaceState { + self.place_states[place].clone() } /// This method exists solely for handling `*`-import visibility constraints. @@ -863,10 +840,10 @@ impl<'db> UseDefMapBuilder<'db> { /// Doing things this way is cheaper in and of itself. However, it also allows us to avoid /// calling [`Self::simplify_visibility_constraints`] after the constraint has been applied to /// the "if-predicate-true" branch and negated for the "if-predicate-false" branch. Simplifying - /// the visibility constraints is only important for symbols that did not have any new + /// the visibility constraints is only important for places that did not have any new /// definitions inside either the "if-predicate-true" branch or the "if-predicate-false" branch. /// - /// - We only snapshot the state for a single symbol prior to the definition, rather than doing + /// - We only snapshot the state for a single place prior to the definition, rather than doing /// expensive calls to [`Self::snapshot`]. Again, this is possible because we know /// that only a single definition occurs inside the "if-predicate-true" predicate branch. /// @@ -880,8 +857,8 @@ impl<'db> UseDefMapBuilder<'db> { pub(super) fn record_and_negate_star_import_visibility_constraint( &mut self, star_import: StarImportPlaceholderPredicate<'db>, - symbol: ScopedSymbolId, - pre_definition_state: SymbolState, + symbol: ScopedPlaceId, + pre_definition_state: PlaceState, ) { let predicate_id = self.add_predicate(star_import.into()); let visibility_id = self.visibility_constraints.add_atom(predicate_id); @@ -890,22 +867,22 @@ impl<'db> UseDefMapBuilder<'db> { .add_not_constraint(visibility_id); let mut post_definition_state = - std::mem::replace(&mut self.symbol_states[symbol], pre_definition_state); + std::mem::replace(&mut self.place_states[symbol], pre_definition_state); post_definition_state .record_visibility_constraint(&mut self.visibility_constraints, visibility_id); - self.symbol_states[symbol] + self.place_states[symbol] .record_visibility_constraint(&mut self.visibility_constraints, negated_visibility_id); - self.symbol_states[symbol].merge( + self.place_states[symbol].merge( post_definition_state, &mut self.narrowing_constraints, &mut self.visibility_constraints, ); } - /// This method resets the visibility constraints for all symbols to a previous state + /// This method resets the visibility constraints for all places to a previous state /// *if* there have been no new declarations or bindings since then. Consider the /// following example: /// ```py @@ -924,10 +901,7 @@ impl<'db> UseDefMapBuilder<'db> { /// constraint for the `x = 0` binding as well, but at the `RESET` point, we can get rid /// of it, as the `if`-`elif`-`elif` chain doesn't include any new bindings of `x`. pub(super) fn simplify_visibility_constraints(&mut self, snapshot: FlowSnapshot) { - debug_assert!(self.symbol_states.len() >= snapshot.symbol_states.len()); - debug_assert!( - self.instance_attribute_states.len() >= snapshot.instance_attribute_states.len() - ); + debug_assert!(self.place_states.len() >= snapshot.place_states.len()); // If there are any control flow paths that have become unreachable between `snapshot` and // now, then it's not valid to simplify any visibility constraints to `snapshot`. @@ -935,20 +909,13 @@ impl<'db> UseDefMapBuilder<'db> { return; } - // Note that this loop terminates when we reach a symbol not present in the snapshot. - // This means we keep visibility constraints for all new symbols, which is intended, - // since these symbols have been introduced in the corresponding branch, which might + // Note that this loop terminates when we reach a place not present in the snapshot. + // This means we keep visibility constraints for all new places, which is intended, + // since these places have been introduced in the corresponding branch, which might // be subject to visibility constraints. We only simplify/reset visibility constraints - // for symbols that have the same bindings and declarations present compared to the + // for places that have the same bindings and declarations present compared to the // snapshot. - for (current, snapshot) in self.symbol_states.iter_mut().zip(snapshot.symbol_states) { - current.simplify_visibility_constraints(snapshot); - } - for (current, snapshot) in self - .instance_attribute_states - .iter_mut() - .zip(snapshot.instance_attribute_states) - { + for (current, snapshot) in self.place_states.iter_mut().zip(snapshot.place_states) { current.simplify_visibility_constraints(snapshot); } } @@ -965,43 +932,64 @@ impl<'db> UseDefMapBuilder<'db> { pub(super) fn record_declaration( &mut self, - symbol: ScopedSymbolId, + place: ScopedPlaceId, declaration: Definition<'db>, ) { - let def_id = self.all_definitions.push(Some(declaration)); - let symbol_state = &mut self.symbol_states[symbol]; + let def_id = self + .all_definitions + .push(DefinitionState::Defined(declaration)); + let place_state = &mut self.place_states[place]; self.bindings_by_declaration - .insert(declaration, symbol_state.bindings().clone()); - symbol_state.record_declaration(def_id); + .insert(declaration, place_state.bindings().clone()); + place_state.record_declaration(def_id); } pub(super) fn record_declaration_and_binding( &mut self, - symbol: ScopedSymbolId, + place: ScopedPlaceId, definition: Definition<'db>, + is_place_name: bool, ) { // We don't need to store anything in self.bindings_by_declaration or // self.declarations_by_binding. - let def_id = self.all_definitions.push(Some(definition)); - let symbol_state = &mut self.symbol_states[symbol]; - symbol_state.record_declaration(def_id); - symbol_state.record_binding(def_id, self.scope_start_visibility, self.is_class_scope); + let def_id = self + .all_definitions + .push(DefinitionState::Defined(definition)); + let place_state = &mut self.place_states[place]; + place_state.record_declaration(def_id); + place_state.record_binding( + def_id, + self.scope_start_visibility, + self.is_class_scope, + is_place_name, + ); + } + + pub(super) fn delete_binding(&mut self, place: ScopedPlaceId, is_place_name: bool) { + let def_id = self.all_definitions.push(DefinitionState::Deleted); + let place_state = &mut self.place_states[place]; + place_state.record_binding( + def_id, + self.scope_start_visibility, + self.is_class_scope, + is_place_name, + ); } pub(super) fn record_use( &mut self, - symbol: ScopedSymbolId, + place: ScopedPlaceId, use_id: ScopedUseId, node_key: NodeKey, ) { - // We have a use of a symbol; clone the current bindings for that symbol, and record them + // We have a use of a place; clone the current bindings for that place, and record them // as the live bindings for this use. let new_use = self .bindings_by_use - .push(self.symbol_states[symbol].bindings().clone()); + .push(self.place_states[place].bindings().clone()); debug_assert_eq!(use_id, new_use); - // Track reachability of all uses of symbols to silence `unresolved-reference` + // Track reachability of all uses of places to silence `unresolved-reference` // diagnostics in unreachable code. self.record_node_reachability(node_key); } @@ -1012,66 +1000,59 @@ impl<'db> UseDefMapBuilder<'db> { pub(super) fn snapshot_eager_state( &mut self, - enclosing_symbol: ScopedSymbolId, + enclosing_place: ScopedPlaceId, scope: ScopeKind, - is_bound: bool, + enclosing_place_expr: &PlaceExpr, ) -> ScopedEagerSnapshotId { - // Names bound in class scopes are never visible to nested scopes, so we never need to - // save eager scope bindings in a class scope. - if scope.is_class() || !is_bound { + // Names bound in class scopes are never visible to nested scopes (but attributes/subscripts are visible), + // so we never need to save eager scope bindings in a class scope. + if (scope.is_class() && enclosing_place_expr.is_name()) || !enclosing_place_expr.is_bound() + { self.eager_snapshots.push(EagerSnapshot::Constraint( - self.symbol_states[enclosing_symbol] + self.place_states[enclosing_place] .bindings() .unbound_narrowing_constraint(), )) } else { self.eager_snapshots.push(EagerSnapshot::Bindings( - self.symbol_states[enclosing_symbol].bindings().clone(), + self.place_states[enclosing_place].bindings().clone(), )) } } - /// Take a snapshot of the current visible-symbols state. + /// Take a snapshot of the current visible-places state. pub(super) fn snapshot(&self) -> FlowSnapshot { FlowSnapshot { - symbol_states: self.symbol_states.clone(), - instance_attribute_states: self.instance_attribute_states.clone(), + place_states: self.place_states.clone(), scope_start_visibility: self.scope_start_visibility, reachability: self.reachability, } } - /// Restore the current builder symbols state to the given snapshot. + /// Restore the current builder places state to the given snapshot. pub(super) fn restore(&mut self, snapshot: FlowSnapshot) { - // We never remove symbols from `symbol_states` (it's an IndexVec, and the symbol - // IDs must line up), so the current number of known symbols must always be equal to or - // greater than the number of known symbols in a previously-taken snapshot. - let num_symbols = self.symbol_states.len(); - debug_assert!(num_symbols >= snapshot.symbol_states.len()); - let num_attributes = self.instance_attribute_states.len(); - debug_assert!(num_attributes >= snapshot.instance_attribute_states.len()); + // We never remove places from `place_states` (it's an IndexVec, and the place + // IDs must line up), so the current number of known places must always be equal to or + // greater than the number of known places in a previously-taken snapshot. + let num_places = self.place_states.len(); + debug_assert!(num_places >= snapshot.place_states.len()); // Restore the current visible-definitions state to the given snapshot. - self.symbol_states = snapshot.symbol_states; - self.instance_attribute_states = snapshot.instance_attribute_states; + self.place_states = snapshot.place_states; self.scope_start_visibility = snapshot.scope_start_visibility; self.reachability = snapshot.reachability; - // If the snapshot we are restoring is missing some symbols we've recorded since, we need - // to fill them in so the symbol IDs continue to line up. Since they don't exist in the + // If the snapshot we are restoring is missing some places we've recorded since, we need + // to fill them in so the place IDs continue to line up. Since they don't exist in the // snapshot, the correct state to fill them in with is "undefined". - self.symbol_states.resize( - num_symbols, - SymbolState::undefined(self.scope_start_visibility), - ); - self.instance_attribute_states.resize( - num_attributes, - SymbolState::undefined(self.scope_start_visibility), + self.place_states.resize( + num_places, + PlaceState::undefined(self.scope_start_visibility), ); } /// Merge the given snapshot into the current state, reflecting that we might have taken either - /// path to get here. The new state for each symbol should include definitions from both the + /// path to get here. The new state for each place should include definitions from both the /// prior state and the snapshot. pub(super) fn merge(&mut self, snapshot: FlowSnapshot) { // As an optimization, if we know statically that either of the snapshots is always @@ -1089,33 +1070,13 @@ impl<'db> UseDefMapBuilder<'db> { return; } - // We never remove symbols from `symbol_states` (it's an IndexVec, and the symbol - // IDs must line up), so the current number of known symbols must always be equal to or - // greater than the number of known symbols in a previously-taken snapshot. - debug_assert!(self.symbol_states.len() >= snapshot.symbol_states.len()); - debug_assert!( - self.instance_attribute_states.len() >= snapshot.instance_attribute_states.len() - ); + // We never remove places from `place_states` (it's an IndexVec, and the place + // IDs must line up), so the current number of known places must always be equal to or + // greater than the number of known places in a previously-taken snapshot. + debug_assert!(self.place_states.len() >= snapshot.place_states.len()); - let mut snapshot_definitions_iter = snapshot.symbol_states.into_iter(); - for current in &mut self.symbol_states { - if let Some(snapshot) = snapshot_definitions_iter.next() { - current.merge( - snapshot, - &mut self.narrowing_constraints, - &mut self.visibility_constraints, - ); - } else { - current.merge( - SymbolState::undefined(snapshot.scope_start_visibility), - &mut self.narrowing_constraints, - &mut self.visibility_constraints, - ); - // Symbol not present in snapshot, so it's unbound/undeclared from that path. - } - } - let mut snapshot_definitions_iter = snapshot.instance_attribute_states.into_iter(); - for current in &mut self.instance_attribute_states { + let mut snapshot_definitions_iter = snapshot.place_states.into_iter(); + for current in &mut self.place_states { if let Some(snapshot) = snapshot_definitions_iter.next() { current.merge( snapshot, @@ -1124,10 +1085,11 @@ impl<'db> UseDefMapBuilder<'db> { ); } else { current.merge( - SymbolState::undefined(snapshot.scope_start_visibility), + PlaceState::undefined(snapshot.scope_start_visibility), &mut self.narrowing_constraints, &mut self.visibility_constraints, ); + // Place not present in snapshot, so it's unbound/undeclared from that path. } } @@ -1142,8 +1104,7 @@ impl<'db> UseDefMapBuilder<'db> { pub(super) fn finish(mut self) -> UseDefMap<'db> { self.all_definitions.shrink_to_fit(); - self.symbol_states.shrink_to_fit(); - self.instance_attribute_states.shrink_to_fit(); + self.place_states.shrink_to_fit(); self.bindings_by_use.shrink_to_fit(); self.node_reachability.shrink_to_fit(); self.declarations_by_binding.shrink_to_fit(); @@ -1157,8 +1118,7 @@ impl<'db> UseDefMapBuilder<'db> { visibility_constraints: self.visibility_constraints.build(), bindings_by_use: self.bindings_by_use, node_reachability: self.node_reachability, - public_symbols: self.symbol_states, - instance_attributes: self.instance_attribute_states, + public_places: self.place_states, declarations_by_binding: self.declarations_by_binding, bindings_by_declaration: self.bindings_by_declaration, eager_snapshots: self.eager_snapshots, diff --git a/crates/ty_python_semantic/src/semantic_index/use_def/symbol_state.rs b/crates/ty_python_semantic/src/semantic_index/use_def/place_state.rs similarity index 84% rename from crates/ty_python_semantic/src/semantic_index/use_def/symbol_state.rs rename to crates/ty_python_semantic/src/semantic_index/use_def/place_state.rs index 0a7df85a83f73..d73134ead65a7 100644 --- a/crates/ty_python_semantic/src/semantic_index/use_def/symbol_state.rs +++ b/crates/ty_python_semantic/src/semantic_index/use_def/place_state.rs @@ -1,4 +1,4 @@ -//! Track live bindings per symbol, applicable constraints per binding, and live declarations. +//! Track live bindings per place, applicable constraints per binding, and live declarations. //! //! These data structures operate entirely on scope-local newtype-indices for definitions and //! constraints, referring to their location in the `all_definitions` and `all_constraints` @@ -60,9 +60,9 @@ pub(super) struct ScopedDefinitionId; impl ScopedDefinitionId { /// A special ID that is used to describe an implicit start-of-scope state. When - /// we see that this definition is live, we know that the symbol is (possibly) + /// we see that this definition is live, we know that the place is (possibly) /// unbound or undeclared at a given usage site. - /// When creating a use-def-map builder, we always add an empty `None` definition + /// When creating a use-def-map builder, we always add an empty `DefinitionState::Undefined` definition /// at index 0, so this ID is always present. pub(super) const UNBOUND: ScopedDefinitionId = ScopedDefinitionId::from_u32(0); @@ -71,19 +71,19 @@ impl ScopedDefinitionId { } } -/// Can keep inline this many live bindings or declarations per symbol at a given time; more will +/// Can keep inline this many live bindings or declarations per place at a given time; more will /// go to heap. -const INLINE_DEFINITIONS_PER_SYMBOL: usize = 4; +const INLINE_DEFINITIONS_PER_PLACE: usize = 4; -/// Live declarations for a single symbol at some point in control flow, with their +/// Live declarations for a single place at some point in control flow, with their /// corresponding visibility constraints. #[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update)] -pub(super) struct SymbolDeclarations { - /// A list of live declarations for this symbol, sorted by their `ScopedDefinitionId` - live_declarations: SmallVec<[LiveDeclaration; INLINE_DEFINITIONS_PER_SYMBOL]>, +pub(super) struct Declarations { + /// A list of live declarations for this place, sorted by their `ScopedDefinitionId` + live_declarations: SmallVec<[LiveDeclaration; INLINE_DEFINITIONS_PER_PLACE]>, } -/// One of the live declarations for a single symbol at some point in control flow. +/// One of the live declarations for a single place at some point in control flow. #[derive(Clone, Debug, PartialEq, Eq)] pub(super) struct LiveDeclaration { pub(super) declaration: ScopedDefinitionId, @@ -92,7 +92,7 @@ pub(super) struct LiveDeclaration { pub(super) type LiveDeclarationsIterator<'a> = std::slice::Iter<'a, LiveDeclaration>; -impl SymbolDeclarations { +impl Declarations { fn undeclared(scope_start_visibility: ScopedVisibilityConstraintId) -> Self { let initial_declaration = LiveDeclaration { declaration: ScopedDefinitionId::UNBOUND, @@ -103,7 +103,7 @@ impl SymbolDeclarations { } } - /// Record a newly-encountered declaration for this symbol. + /// Record a newly-encountered declaration for this place. fn record_declaration(&mut self, declaration: ScopedDefinitionId) { // The new declaration replaces all previous live declaration in this path. self.live_declarations.clear(); @@ -125,17 +125,17 @@ impl SymbolDeclarations { } } - /// Return an iterator over live declarations for this symbol. + /// Return an iterator over live declarations for this place. pub(super) fn iter(&self) -> LiveDeclarationsIterator<'_> { self.live_declarations.iter() } - /// Iterate over the IDs of each currently live declaration for this symbol + /// Iterate over the IDs of each currently live declaration for this place fn iter_declarations(&self) -> impl Iterator + '_ { self.iter().map(|lb| lb.declaration) } - fn simplify_visibility_constraints(&mut self, other: SymbolDeclarations) { + fn simplify_visibility_constraints(&mut self, other: Declarations) { // If the set of live declarations hasn't changed, don't simplify. if self.live_declarations.len() != other.live_declarations.len() || !self.iter_declarations().eq(other.iter_declarations()) @@ -181,7 +181,7 @@ impl SymbolDeclarations { } } -/// A snapshot of a symbol state that can be used to resolve a reference in a nested eager scope. +/// A snapshot of a place state that can be used to resolve a reference in a nested eager scope. /// If there are bindings in a (non-class) scope , they are stored in `Bindings`. /// Even if it's a class scope (class variables are not visible to nested scopes) or there are no /// bindings, the current narrowing constraint is necessary for narrowing, so it's stored in @@ -189,34 +189,30 @@ impl SymbolDeclarations { #[derive(Clone, Debug, PartialEq, Eq, salsa::Update)] pub(super) enum EagerSnapshot { Constraint(ScopedNarrowingConstraint), - Bindings(SymbolBindings), + Bindings(Bindings), } -/// Live bindings for a single symbol at some point in control flow. Each live binding comes +/// Live bindings for a single place at some point in control flow. Each live binding comes /// with a set of narrowing constraints and a visibility constraint. #[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update)] -pub(super) struct SymbolBindings { +pub(super) struct Bindings { /// The narrowing constraint applicable to the "unbound" binding, if we need access to it even - /// when it's not visible. This happens in class scopes, where local bindings are not visible + /// when it's not visible. This happens in class scopes, where local name bindings are not visible /// to nested scopes, but we still need to know what narrowing constraints were applied to the /// "unbound" binding. unbound_narrowing_constraint: Option, - /// A list of live bindings for this symbol, sorted by their `ScopedDefinitionId` - live_bindings: SmallVec<[LiveBinding; INLINE_DEFINITIONS_PER_SYMBOL]>, + /// A list of live bindings for this place, sorted by their `ScopedDefinitionId` + live_bindings: SmallVec<[LiveBinding; INLINE_DEFINITIONS_PER_PLACE]>, } -impl SymbolBindings { +impl Bindings { pub(super) fn unbound_narrowing_constraint(&self) -> ScopedNarrowingConstraint { - debug_assert!( - self.unbound_narrowing_constraint.is_some() - || self.live_bindings[0].binding.is_unbound() - ); self.unbound_narrowing_constraint .unwrap_or(self.live_bindings[0].narrowing_constraint) } } -/// One of the live bindings for a single symbol at some point in control flow. +/// One of the live bindings for a single place at some point in control flow. #[derive(Clone, Debug, PartialEq, Eq)] pub(super) struct LiveBinding { pub(super) binding: ScopedDefinitionId, @@ -226,7 +222,7 @@ pub(super) struct LiveBinding { pub(super) type LiveBindingsIterator<'a> = std::slice::Iter<'a, LiveBinding>; -impl SymbolBindings { +impl Bindings { fn unbound(scope_start_visibility: ScopedVisibilityConstraintId) -> Self { let initial_binding = LiveBinding { binding: ScopedDefinitionId::UNBOUND, @@ -239,16 +235,17 @@ impl SymbolBindings { } } - /// Record a newly-encountered binding for this symbol. + /// Record a newly-encountered binding for this place. pub(super) fn record_binding( &mut self, binding: ScopedDefinitionId, visibility_constraint: ScopedVisibilityConstraintId, is_class_scope: bool, + is_place_name: bool, ) { - // If we are in a class scope, and the unbound binding was previously visible, but we will + // If we are in a class scope, and the unbound name binding was previously visible, but we will // now replace it, record the narrowing constraints on it: - if is_class_scope && self.live_bindings[0].binding.is_unbound() { + if is_class_scope && is_place_name && self.live_bindings[0].binding.is_unbound() { self.unbound_narrowing_constraint = Some(self.live_bindings[0].narrowing_constraint); } // The new binding replaces all previous live bindings in this path, and has no @@ -285,17 +282,17 @@ impl SymbolBindings { } } - /// Iterate over currently live bindings for this symbol + /// Iterate over currently live bindings for this place pub(super) fn iter(&self) -> LiveBindingsIterator<'_> { self.live_bindings.iter() } - /// Iterate over the IDs of each currently live binding for this symbol + /// Iterate over the IDs of each currently live binding for this place fn iter_bindings(&self) -> impl Iterator + '_ { self.iter().map(|lb| lb.binding) } - fn simplify_visibility_constraints(&mut self, other: SymbolBindings) { + fn simplify_visibility_constraints(&mut self, other: Bindings) { // If the set of live bindings hasn't changed, don't simplify. if self.live_bindings.len() != other.live_bindings.len() || !self.iter_bindings().eq(other.iter_bindings()) @@ -360,30 +357,35 @@ impl SymbolBindings { } #[derive(Clone, Debug, PartialEq, Eq)] -pub(in crate::semantic_index) struct SymbolState { - declarations: SymbolDeclarations, - bindings: SymbolBindings, +pub(in crate::semantic_index) struct PlaceState { + declarations: Declarations, + bindings: Bindings, } -impl SymbolState { - /// Return a new [`SymbolState`] representing an unbound, undeclared symbol. +impl PlaceState { + /// Return a new [`PlaceState`] representing an unbound, undeclared place. pub(super) fn undefined(scope_start_visibility: ScopedVisibilityConstraintId) -> Self { Self { - declarations: SymbolDeclarations::undeclared(scope_start_visibility), - bindings: SymbolBindings::unbound(scope_start_visibility), + declarations: Declarations::undeclared(scope_start_visibility), + bindings: Bindings::unbound(scope_start_visibility), } } - /// Record a newly-encountered binding for this symbol. + /// Record a newly-encountered binding for this place. pub(super) fn record_binding( &mut self, binding_id: ScopedDefinitionId, visibility_constraint: ScopedVisibilityConstraintId, is_class_scope: bool, + is_place_name: bool, ) { debug_assert_ne!(binding_id, ScopedDefinitionId::UNBOUND); - self.bindings - .record_binding(binding_id, visibility_constraint, is_class_scope); + self.bindings.record_binding( + binding_id, + visibility_constraint, + is_class_scope, + is_place_name, + ); } /// Add given constraint to all live bindings. @@ -409,24 +411,24 @@ impl SymbolState { } /// Simplifies this snapshot to have the same visibility constraints as a previous point in the - /// control flow, but only if the set of live bindings or declarations for this symbol hasn't + /// control flow, but only if the set of live bindings or declarations for this place hasn't /// changed. - pub(super) fn simplify_visibility_constraints(&mut self, snapshot_state: SymbolState) { + pub(super) fn simplify_visibility_constraints(&mut self, snapshot_state: PlaceState) { self.bindings .simplify_visibility_constraints(snapshot_state.bindings); self.declarations .simplify_visibility_constraints(snapshot_state.declarations); } - /// Record a newly-encountered declaration of this symbol. + /// Record a newly-encountered declaration of this place. pub(super) fn record_declaration(&mut self, declaration_id: ScopedDefinitionId) { self.declarations.record_declaration(declaration_id); } - /// Merge another [`SymbolState`] into this one. + /// Merge another [`PlaceState`] into this one. pub(super) fn merge( &mut self, - b: SymbolState, + b: PlaceState, narrowing_constraints: &mut NarrowingConstraintsBuilder, visibility_constraints: &mut VisibilityConstraintsBuilder, ) { @@ -436,11 +438,11 @@ impl SymbolState { .merge(b.declarations, visibility_constraints); } - pub(super) fn bindings(&self) -> &SymbolBindings { + pub(super) fn bindings(&self) -> &Bindings { &self.bindings } - pub(super) fn declarations(&self) -> &SymbolDeclarations { + pub(super) fn declarations(&self) -> &Declarations { &self.declarations } } @@ -454,10 +456,10 @@ mod tests { #[track_caller] fn assert_bindings( narrowing_constraints: &NarrowingConstraintsBuilder, - symbol: &SymbolState, + place: &PlaceState, expected: &[&str], ) { - let actual = symbol + let actual = place .bindings() .iter() .map(|live_binding| { @@ -479,8 +481,8 @@ mod tests { } #[track_caller] - pub(crate) fn assert_declarations(symbol: &SymbolState, expected: &[&str]) { - let actual = symbol + pub(crate) fn assert_declarations(place: &PlaceState, expected: &[&str]) { + let actual = place .declarations() .iter() .map( @@ -502,7 +504,7 @@ mod tests { #[test] fn unbound() { let narrowing_constraints = NarrowingConstraintsBuilder::default(); - let sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); + let sym = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); assert_bindings(&narrowing_constraints, &sym, &["unbound<>"]); } @@ -510,11 +512,12 @@ mod tests { #[test] fn with() { let narrowing_constraints = NarrowingConstraintsBuilder::default(); - let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); + let mut sym = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); sym.record_binding( ScopedDefinitionId::from_u32(1), ScopedVisibilityConstraintId::ALWAYS_TRUE, false, + true, ); assert_bindings(&narrowing_constraints, &sym, &["1<>"]); @@ -523,11 +526,12 @@ mod tests { #[test] fn record_constraint() { let mut narrowing_constraints = NarrowingConstraintsBuilder::default(); - let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); + let mut sym = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); sym.record_binding( ScopedDefinitionId::from_u32(1), ScopedVisibilityConstraintId::ALWAYS_TRUE, false, + true, ); let predicate = ScopedPredicateId::from_u32(0).into(); sym.record_narrowing_constraint(&mut narrowing_constraints, predicate); @@ -541,20 +545,22 @@ mod tests { let mut visibility_constraints = VisibilityConstraintsBuilder::default(); // merging the same definition with the same constraint keeps the constraint - let mut sym1a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); + let mut sym1a = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); sym1a.record_binding( ScopedDefinitionId::from_u32(1), ScopedVisibilityConstraintId::ALWAYS_TRUE, false, + true, ); let predicate = ScopedPredicateId::from_u32(0).into(); sym1a.record_narrowing_constraint(&mut narrowing_constraints, predicate); - let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); + let mut sym1b = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); sym1b.record_binding( ScopedDefinitionId::from_u32(1), ScopedVisibilityConstraintId::ALWAYS_TRUE, false, + true, ); let predicate = ScopedPredicateId::from_u32(0).into(); sym1b.record_narrowing_constraint(&mut narrowing_constraints, predicate); @@ -568,20 +574,22 @@ mod tests { assert_bindings(&narrowing_constraints, &sym1, &["1<0>"]); // merging the same definition with differing constraints drops all constraints - let mut sym2a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); + let mut sym2a = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); sym2a.record_binding( ScopedDefinitionId::from_u32(2), ScopedVisibilityConstraintId::ALWAYS_TRUE, false, + true, ); let predicate = ScopedPredicateId::from_u32(1).into(); sym2a.record_narrowing_constraint(&mut narrowing_constraints, predicate); - let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); + let mut sym1b = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); sym1b.record_binding( ScopedDefinitionId::from_u32(2), ScopedVisibilityConstraintId::ALWAYS_TRUE, false, + true, ); let predicate = ScopedPredicateId::from_u32(2).into(); sym1b.record_narrowing_constraint(&mut narrowing_constraints, predicate); @@ -595,16 +603,17 @@ mod tests { assert_bindings(&narrowing_constraints, &sym2, &["2<>"]); // merging a constrained definition with unbound keeps both - let mut sym3a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); + let mut sym3a = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); sym3a.record_binding( ScopedDefinitionId::from_u32(3), ScopedVisibilityConstraintId::ALWAYS_TRUE, false, + true, ); let predicate = ScopedPredicateId::from_u32(3).into(); sym3a.record_narrowing_constraint(&mut narrowing_constraints, predicate); - let sym2b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); + let sym2b = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); sym3a.merge( sym2b, @@ -626,14 +635,14 @@ mod tests { #[test] fn no_declaration() { - let sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); + let sym = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); assert_declarations(&sym, &["undeclared"]); } #[test] fn record_declaration() { - let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); + let mut sym = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); sym.record_declaration(ScopedDefinitionId::from_u32(1)); assert_declarations(&sym, &["1"]); @@ -641,7 +650,7 @@ mod tests { #[test] fn record_declaration_override() { - let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); + let mut sym = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); sym.record_declaration(ScopedDefinitionId::from_u32(1)); sym.record_declaration(ScopedDefinitionId::from_u32(2)); @@ -652,10 +661,10 @@ mod tests { fn record_declaration_merge() { let mut narrowing_constraints = NarrowingConstraintsBuilder::default(); let mut visibility_constraints = VisibilityConstraintsBuilder::default(); - let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); + let mut sym = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); sym.record_declaration(ScopedDefinitionId::from_u32(1)); - let mut sym2 = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); + let mut sym2 = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); sym2.record_declaration(ScopedDefinitionId::from_u32(2)); sym.merge( @@ -671,10 +680,10 @@ mod tests { fn record_declaration_merge_partial_undeclared() { let mut narrowing_constraints = NarrowingConstraintsBuilder::default(); let mut visibility_constraints = VisibilityConstraintsBuilder::default(); - let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); + let mut sym = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); sym.record_declaration(ScopedDefinitionId::from_u32(1)); - let sym2 = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); + let sym2 = PlaceState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); sym.merge( sym2, diff --git a/crates/ty_python_semantic/src/semantic_index/visibility_constraints.rs b/crates/ty_python_semantic/src/semantic_index/visibility_constraints.rs index 4e2e76e301fff..60db138c57da9 100644 --- a/crates/ty_python_semantic/src/semantic_index/visibility_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/visibility_constraints.rs @@ -180,12 +180,12 @@ use rustc_hash::FxHashMap; use crate::Db; use crate::dunder_all::dunder_all_names; +use crate::place::{RequiresExplicitReExport, imported_symbol}; use crate::semantic_index::expression::Expression; +use crate::semantic_index::place_table; use crate::semantic_index::predicate::{ PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, Predicates, ScopedPredicateId, }; -use crate::semantic_index::symbol_table; -use crate::symbol::{RequiresExplicitReExport, imported_symbol}; use crate::types::{Truthiness, Type, infer_expression_type}; /// A ternary formula that defines under what conditions a binding is visible. (A ternary formula @@ -654,8 +654,10 @@ impl VisibilityConstraints { } PredicateNode::Pattern(inner) => Self::analyze_single_pattern_predicate(db, inner), PredicateNode::StarImportPlaceholder(star_import) => { - let symbol_table = symbol_table(db, star_import.scope(db)); - let symbol_name = symbol_table.symbol(star_import.symbol_id(db)).name(); + let place_table = place_table(db, star_import.scope(db)); + let symbol_name = place_table + .place_expr(star_import.symbol_id(db)) + .expect_name(); let referenced_file = star_import.referenced_file(db); let requires_explicit_reexport = match dunder_all_names(db, referenced_file) { @@ -675,15 +677,15 @@ impl VisibilityConstraints { }; match imported_symbol(db, referenced_file, symbol_name, requires_explicit_reexport) - .symbol + .place { - crate::symbol::Symbol::Type(_, crate::symbol::Boundness::Bound) => { + crate::place::Place::Type(_, crate::place::Boundness::Bound) => { Truthiness::AlwaysTrue } - crate::symbol::Symbol::Type(_, crate::symbol::Boundness::PossiblyUnbound) => { + crate::place::Place::Type(_, crate::place::Boundness::PossiblyUnbound) => { Truthiness::Ambiguous } - crate::symbol::Symbol::Unbound => Truthiness::AlwaysFalse, + crate::place::Place::Unbound => Truthiness::AlwaysFalse, } } } diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index bc4204386e016..4a85f138a32df 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -8,8 +8,8 @@ use crate::Db; use crate::module_name::ModuleName; use crate::module_resolver::{Module, resolve_module}; use crate::semantic_index::ast_ids::HasScopedExpressionId; +use crate::semantic_index::place::FileScopeId; use crate::semantic_index::semantic_index; -use crate::semantic_index::symbol::FileScopeId; use crate::types::ide_support::all_declarations_and_bindings; use crate::types::{Type, binding_type, infer_scope_types}; diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index b2d1c6b91f1ac..ebccf4a0e7bb4 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -31,12 +31,12 @@ pub(crate) use self::signatures::{CallableSignature, Signature}; pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType}; use crate::module_name::ModuleName; use crate::module_resolver::{KnownModule, resolve_module}; +use crate::place::{Boundness, Place, PlaceAndQualifiers, imported_symbol}; use crate::semantic_index::ast_ids::HasScopedExpressionId; use crate::semantic_index::definition::Definition; -use crate::semantic_index::symbol::ScopeId; +use crate::semantic_index::place::ScopeId; use crate::semantic_index::{imported_modules, semantic_index}; use crate::suppression::check_suppressions; -use crate::symbol::{Boundness, Symbol, SymbolAndQualifiers, imported_symbol}; use crate::types::call::{Binding, Bindings, CallArgumentTypes, CallableBinding}; pub(crate) use crate::types::class_base::ClassBase; use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder}; @@ -242,12 +242,12 @@ impl Default for MemberLookupPolicy { fn member_lookup_cycle_recover<'db>( _db: &'db dyn Db, - _value: &SymbolAndQualifiers<'db>, + _value: &PlaceAndQualifiers<'db>, _count: u32, _self: Type<'db>, _name: Name, _policy: MemberLookupPolicy, -) -> salsa::CycleRecoveryAction> { +) -> salsa::CycleRecoveryAction> { salsa::CycleRecoveryAction::Iterate } @@ -256,8 +256,28 @@ fn member_lookup_cycle_initial<'db>( _self: Type<'db>, _name: Name, _policy: MemberLookupPolicy, -) -> SymbolAndQualifiers<'db> { - Symbol::bound(Type::Never).into() +) -> PlaceAndQualifiers<'db> { + Place::bound(Type::Never).into() +} + +fn class_lookup_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &PlaceAndQualifiers<'db>, + _count: u32, + _self: Type<'db>, + _name: Name, + _policy: MemberLookupPolicy, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn class_lookup_cycle_initial<'db>( + _db: &'db dyn Db, + _self: Type<'db>, + _name: Name, + _policy: MemberLookupPolicy, +) -> PlaceAndQualifiers<'db> { + Place::bound(Type::Never).into() } /// Meta data for `Type::Todo`, which represents a known limitation in ty. @@ -1262,11 +1282,11 @@ impl<'db> Type<'db> { Name::new_static("__call__"), MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) - .symbol; + .place; // If the type of __call__ is a subtype of a callable type, this instance is. // Don't add other special cases here; our subtyping of a callable type // shouldn't get out of sync with the calls we will actually allow. - if let Symbol::Type(t, Boundness::Bound) = call_symbol { + if let Place::Type(t, Boundness::Bound) = call_symbol { t.is_subtype_of(db, target) } else { false @@ -1625,11 +1645,9 @@ impl<'db> Type<'db> { Name::new_static("__call__"), MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) - .symbol; - // If the type of __call__ is assignable to a callable type, this instance is. - // Don't add other special cases here; our assignability to a callable type + .place; // shouldn't get out of sync with the calls we will actually allow. - if let Symbol::Type(t, Boundness::Bound) = call_symbol { + if let Place::Type(t, Boundness::Bound) = call_symbol { t.is_assignable_to(db, target) } else { false @@ -2178,7 +2196,7 @@ impl<'db> Type<'db> { Name::new_static("__call__"), MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) - .symbol + .place .ignore_possibly_unbound() .is_none_or(|dunder_call| { !dunder_call.is_assignable_to(db, CallableType::unknown(db)) @@ -2504,7 +2522,7 @@ impl<'db> Type<'db> { /// /// [descriptor guide]: https://docs.python.org/3/howto/descriptor.html#invocation-from-an-instance /// [`_PyType_Lookup`]: https://github.com/python/cpython/blob/e285232c76606e3be7bf216efb1be1e742423e4b/Objects/typeobject.c#L5223 - fn find_name_in_mro(&self, db: &'db dyn Db, name: &str) -> Option> { + fn find_name_in_mro(&self, db: &'db dyn Db, name: &str) -> Option> { self.find_name_in_mro_with_policy(db, name, MemberLookupPolicy::default()) } @@ -2513,7 +2531,7 @@ impl<'db> Type<'db> { db: &'db dyn Db, name: &str, policy: MemberLookupPolicy, - ) -> Option> { + ) -> Option> { match self { Type::Union(union) => Some(union.map_with_boundness_and_qualifiers(db, |elem| { elem.find_name_in_mro_with_policy(db, name, policy) @@ -2531,28 +2549,28 @@ impl<'db> Type<'db> { })) } - Type::Dynamic(_) | Type::Never => Some(Symbol::bound(self).into()), + Type::Dynamic(_) | Type::Never => Some(Place::bound(self).into()), Type::ClassLiteral(class) => { match (class.known(db), name) { (Some(KnownClass::FunctionType), "__get__") => Some( - Symbol::bound(Type::WrapperDescriptor( + Place::bound(Type::WrapperDescriptor( WrapperDescriptorKind::FunctionTypeDunderGet, )) .into(), ), (Some(KnownClass::FunctionType), "__set__" | "__delete__") => { // Hard code this knowledge, as we look up `__set__` and `__delete__` on `FunctionType` often. - Some(Symbol::Unbound.into()) + Some(Place::Unbound.into()) } (Some(KnownClass::Property), "__get__") => Some( - Symbol::bound(Type::WrapperDescriptor( + Place::bound(Type::WrapperDescriptor( WrapperDescriptorKind::PropertyDunderGet, )) .into(), ), (Some(KnownClass::Property), "__set__") => Some( - Symbol::bound(Type::WrapperDescriptor( + Place::bound(Type::WrapperDescriptor( WrapperDescriptorKind::PropertyDunderSet, )) .into(), @@ -2582,7 +2600,7 @@ impl<'db> Type<'db> { // MRO of the class `object`. Type::NominalInstance(instance) if instance.class.is_known(db, KnownClass::Type) => { if policy.mro_no_object_fallback() { - Some(Symbol::Unbound.into()) + Some(Place::Unbound.into()) } else { KnownClass::Object .to_class_literal(db) @@ -2620,17 +2638,17 @@ impl<'db> Type<'db> { /// /// Basically corresponds to `self.to_meta_type().find_name_in_mro(name)`, except for the handling /// of union and intersection types. - fn class_member(self, db: &'db dyn Db, name: Name) -> SymbolAndQualifiers<'db> { + fn class_member(self, db: &'db dyn Db, name: Name) -> PlaceAndQualifiers<'db> { self.class_member_with_policy(db, name, MemberLookupPolicy::default()) } - #[salsa::tracked] + #[salsa::tracked(cycle_fn=class_lookup_cycle_recover, cycle_initial=class_lookup_cycle_initial)] fn class_member_with_policy( self, db: &'db dyn Db, name: Name, policy: MemberLookupPolicy, - ) -> SymbolAndQualifiers<'db> { + ) -> PlaceAndQualifiers<'db> { tracing::trace!("class_member: {}.{}", self.display(db), name); match self { Type::Union(union) => union.map_with_boundness_and_qualifiers(db, |elem| { @@ -2664,7 +2682,7 @@ impl<'db> Type<'db> { /// def __init__(self): /// self.b: str = "a" /// ``` - fn instance_member(&self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { + fn instance_member(&self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { match self { Type::Union(union) => { union.map_with_boundness_and_qualifiers(db, |elem| elem.instance_member(db, name)) @@ -2673,7 +2691,7 @@ impl<'db> Type<'db> { Type::Intersection(intersection) => intersection .map_with_boundness_and_qualifiers(db, |elem| elem.instance_member(db, name)), - Type::Dynamic(_) | Type::Never => Symbol::bound(self).into(), + Type::Dynamic(_) | Type::Never => Place::bound(self).into(), Type::NominalInstance(instance) => instance.class.instance_member(db, name), @@ -2723,7 +2741,7 @@ impl<'db> Type<'db> { .to_instance(db) .instance_member(db, name), - Type::SpecialForm(_) | Type::KnownInstance(_) => Symbol::Unbound.into(), + Type::SpecialForm(_) | Type::KnownInstance(_) => Place::Unbound.into(), Type::PropertyInstance(_) => KnownClass::Property .to_instance(db) @@ -2741,7 +2759,7 @@ impl<'db> Type<'db> { // required, as `instance_member` is only called for instance-like types through `member`, // but we might want to add this in the future. Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => { - Symbol::Unbound.into() + Place::Unbound.into() } } } @@ -2750,17 +2768,17 @@ impl<'db> Type<'db> { /// method corresponds to `inspect.getattr_static(, name)`. /// /// See also: [`Type::member`] - fn static_member(&self, db: &'db dyn Db, name: &str) -> Symbol<'db> { + fn static_member(&self, db: &'db dyn Db, name: &str) -> Place<'db> { if let Type::ModuleLiteral(module) = self { module.static_member(db, name) - } else if let symbol @ Symbol::Type(_, _) = self.class_member(db, name.into()).symbol { - symbol - } else if let Some(symbol @ Symbol::Type(_, _)) = - self.find_name_in_mro(db, name).map(|inner| inner.symbol) + } else if let place @ Place::Type(_, _) = self.class_member(db, name.into()).place { + place + } else if let Some(place @ Place::Type(_, _)) = + self.find_name_in_mro(db, name).map(|inner| inner.place) { - symbol + place } else { - self.instance_member(db, name).symbol + self.instance_member(db, name).place } } @@ -2806,9 +2824,9 @@ impl<'db> Type<'db> { _ => {} } - let descr_get = self.class_member(db, "__get__".into()).symbol; + let descr_get = self.class_member(db, "__get__".into()).place; - if let Symbol::Type(descr_get, descr_get_boundness) = descr_get { + if let Place::Type(descr_get, descr_get_boundness) = descr_get { let return_ty = descr_get .try_call(db, &CallArgumentTypes::positional([self, instance, owner])) .map(|bindings| { @@ -2820,15 +2838,10 @@ impl<'db> Type<'db> { }) .ok()?; - let descriptor_kind = if self.class_member(db, "__set__".into()).symbol.is_unbound() - && self - .class_member(db, "__delete__".into()) - .symbol - .is_unbound() - { - AttributeKind::NormalOrNonDataDescriptor - } else { + let descriptor_kind = if self.is_data_descriptor(db) { AttributeKind::DataDescriptor + } else { + AttributeKind::NormalOrNonDataDescriptor }; Some((return_ty, descriptor_kind)) @@ -2842,10 +2855,10 @@ impl<'db> Type<'db> { /// and intersections explicitly. fn try_call_dunder_get_on_attribute( db: &'db dyn Db, - attribute: SymbolAndQualifiers<'db>, + attribute: PlaceAndQualifiers<'db>, instance: Type<'db>, owner: Type<'db>, - ) -> (SymbolAndQualifiers<'db>, AttributeKind) { + ) -> (PlaceAndQualifiers<'db>, AttributeKind) { match attribute { // This branch is not strictly needed, but it short-circuits the lookup of various dunder // methods and calls that would otherwise be made. @@ -2855,18 +2868,18 @@ impl<'db> Type<'db> { // data descriptors. // // The same is true for `Never`. - SymbolAndQualifiers { - symbol: Symbol::Type(Type::Dynamic(_) | Type::Never, _), + PlaceAndQualifiers { + place: Place::Type(Type::Dynamic(_) | Type::Never, _), qualifiers: _, } => (attribute, AttributeKind::DataDescriptor), - SymbolAndQualifiers { - symbol: Symbol::Type(Type::Union(union), boundness), + PlaceAndQualifiers { + place: Place::Type(Type::Union(union), boundness), qualifiers, } => ( union .map_with_boundness(db, |elem| { - Symbol::Type( + Place::Type( elem.try_call_dunder_get(db, instance, owner) .map_or(*elem, |(ty, _)| ty), boundness, @@ -2884,13 +2897,13 @@ impl<'db> Type<'db> { }, ), - SymbolAndQualifiers { - symbol: Symbol::Type(Type::Intersection(intersection), boundness), + PlaceAndQualifiers { + place: Place::Type(Type::Intersection(intersection), boundness), qualifiers, } => ( intersection .map_with_boundness(db, |elem| { - Symbol::Type( + Place::Type( elem.try_call_dunder_get(db, instance, owner) .map_or(*elem, |(ty, _)| ty), boundness, @@ -2901,14 +2914,14 @@ impl<'db> Type<'db> { AttributeKind::NormalOrNonDataDescriptor, ), - SymbolAndQualifiers { - symbol: Symbol::Type(attribute_ty, boundness), + PlaceAndQualifiers { + place: Place::Type(attribute_ty, boundness), qualifiers: _, } => { if let Some((return_ty, attribute_kind)) = attribute_ty.try_call_dunder_get(db, instance, owner) { - (Symbol::Type(return_ty, boundness).into(), attribute_kind) + (Place::Type(return_ty, boundness).into(), attribute_kind) } else { (attribute, AttributeKind::NormalOrNonDataDescriptor) } @@ -2918,6 +2931,44 @@ impl<'db> Type<'db> { } } + /// Returns whether this type is a data descriptor, i.e. defines `__set__` or `__delete__`. + /// If this type is a union, requires all elements of union to be data descriptors. + pub(crate) fn is_data_descriptor(self, d: &'db dyn Db) -> bool { + self.is_data_descriptor_impl(d, false) + } + + /// Returns whether this type may be a data descriptor. + /// If this type is a union, returns true if _any_ element is a data descriptor. + pub(crate) fn may_be_data_descriptor(self, d: &'db dyn Db) -> bool { + self.is_data_descriptor_impl(d, true) + } + + fn is_data_descriptor_impl(self, db: &'db dyn Db, any_of_union: bool) -> bool { + match self { + Type::Dynamic(_) | Type::Never | Type::PropertyInstance(_) => true, + Type::Union(union) if any_of_union => union + .elements(db) + .iter() + // Types of instance attributes that are not explicitly typed are unioned with `Unknown`, it should be excluded when checking. + .filter(|ty| !ty.is_unknown()) + .any(|ty| ty.is_data_descriptor_impl(db, any_of_union)), + Type::Union(union) => union + .elements(db) + .iter() + .all(|ty| ty.is_data_descriptor_impl(db, any_of_union)), + Type::Intersection(intersection) => intersection + .iter_positive(db) + .any(|ty| ty.is_data_descriptor_impl(db, any_of_union)), + _ => { + !self.class_member(db, "__set__".into()).place.is_unbound() + || !self + .class_member(db, "__delete__".into()) + .place + .is_unbound() + } + } + } + /// Implementation of the descriptor protocol. /// /// This method roughly performs the following steps: @@ -2936,13 +2987,13 @@ impl<'db> Type<'db> { self, db: &'db dyn Db, name: &str, - fallback: SymbolAndQualifiers<'db>, + fallback: PlaceAndQualifiers<'db>, policy: InstanceFallbackShadowsNonDataDescriptor, member_policy: MemberLookupPolicy, - ) -> SymbolAndQualifiers<'db> { + ) -> PlaceAndQualifiers<'db> { let ( - SymbolAndQualifiers { - symbol: meta_attr, + PlaceAndQualifiers { + place: meta_attr, qualifiers: meta_attr_qualifiers, }, meta_attr_kind, @@ -2953,21 +3004,21 @@ impl<'db> Type<'db> { self.to_meta_type(db), ); - let SymbolAndQualifiers { - symbol: fallback, + let PlaceAndQualifiers { + place: fallback, qualifiers: fallback_qualifiers, } = fallback; match (meta_attr, meta_attr_kind, fallback) { // The fallback type is unbound, so we can just return `meta_attr` unconditionally, // no matter if it's data descriptor, a non-data descriptor, or a normal attribute. - (meta_attr @ Symbol::Type(_, _), _, Symbol::Unbound) => { + (meta_attr @ Place::Type(_, _), _, Place::Unbound) => { meta_attr.with_qualifiers(meta_attr_qualifiers) } // `meta_attr` is the return type of a data descriptor and definitely bound, so we // return it. - (meta_attr @ Symbol::Type(_, Boundness::Bound), AttributeKind::DataDescriptor, _) => { + (meta_attr @ Place::Type(_, Boundness::Bound), AttributeKind::DataDescriptor, _) => { meta_attr.with_qualifiers(meta_attr_qualifiers) } @@ -2975,10 +3026,10 @@ impl<'db> Type<'db> { // meta-type is possibly-unbound. This means that we "fall through" to the next // stage of the descriptor protocol and union with the fallback type. ( - Symbol::Type(meta_attr_ty, Boundness::PossiblyUnbound), + Place::Type(meta_attr_ty, Boundness::PossiblyUnbound), AttributeKind::DataDescriptor, - Symbol::Type(fallback_ty, fallback_boundness), - ) => Symbol::Type( + Place::Type(fallback_ty, fallback_boundness), + ) => Place::Type( UnionType::from_elements(db, [meta_attr_ty, fallback_ty]), fallback_boundness, ) @@ -2993,9 +3044,9 @@ impl<'db> Type<'db> { // would require us to statically infer if an instance attribute is always set, which // is something we currently don't attempt to do. ( - Symbol::Type(_, _), + Place::Type(_, _), AttributeKind::NormalOrNonDataDescriptor, - fallback @ Symbol::Type(_, Boundness::Bound), + fallback @ Place::Type(_, Boundness::Bound), ) if policy == InstanceFallbackShadowsNonDataDescriptor::Yes => { fallback.with_qualifiers(fallback_qualifiers) } @@ -3004,17 +3055,17 @@ impl<'db> Type<'db> { // unbound or the policy argument is `No`. In both cases, the `fallback` type does // not completely shadow the non-data descriptor, so we build a union of the two. ( - Symbol::Type(meta_attr_ty, meta_attr_boundness), + Place::Type(meta_attr_ty, meta_attr_boundness), AttributeKind::NormalOrNonDataDescriptor, - Symbol::Type(fallback_ty, fallback_boundness), - ) => Symbol::Type( + Place::Type(fallback_ty, fallback_boundness), + ) => Place::Type( UnionType::from_elements(db, [meta_attr_ty, fallback_ty]), meta_attr_boundness.max(fallback_boundness), ) .with_qualifiers(meta_attr_qualifiers.union(fallback_qualifiers)), // If the attribute is not found on the meta-type, we simply return the fallback. - (Symbol::Unbound, _, fallback) => fallback.with_qualifiers(fallback_qualifiers), + (Place::Unbound, _, fallback) => fallback.with_qualifiers(fallback_qualifiers), } } @@ -3026,7 +3077,7 @@ impl<'db> Type<'db> { /// TODO: We should return a `Result` here to handle errors that can appear during attribute /// lookup, like a failed `__get__` call on a descriptor. #[must_use] - pub(crate) fn member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { + pub(crate) fn member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { self.member_lookup_with_policy(db, name.into(), MemberLookupPolicy::default()) } @@ -3038,10 +3089,10 @@ impl<'db> Type<'db> { db: &'db dyn Db, name: Name, policy: MemberLookupPolicy, - ) -> SymbolAndQualifiers<'db> { + ) -> PlaceAndQualifiers<'db> { tracing::trace!("member_lookup_with_policy: {}.{}", self.display(db), name); if name == "__class__" { - return Symbol::bound(self.to_meta_type(db)).into(); + return Place::bound(self.to_meta_type(db)).into(); } let name_str = name.as_str(); @@ -3050,36 +3101,36 @@ impl<'db> Type<'db> { Type::Union(union) => union .map_with_boundness(db, |elem| { elem.member_lookup_with_policy(db, name_str.into(), policy) - .symbol + .place }) .into(), Type::Intersection(intersection) => intersection .map_with_boundness(db, |elem| { elem.member_lookup_with_policy(db, name_str.into(), policy) - .symbol + .place }) .into(), - Type::Dynamic(..) | Type::Never => Symbol::bound(self).into(), + Type::Dynamic(..) | Type::Never => Place::bound(self).into(), - Type::FunctionLiteral(function) if name == "__get__" => Symbol::bound( + Type::FunctionLiteral(function) if name == "__get__" => Place::bound( Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)), ) .into(), - Type::FunctionLiteral(function) if name == "__call__" => Symbol::bound( + Type::FunctionLiteral(function) if name == "__call__" => Place::bound( Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderCall(function)), ) .into(), - Type::PropertyInstance(property) if name == "__get__" => Symbol::bound( + Type::PropertyInstance(property) if name == "__get__" => Place::bound( Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(property)), ) .into(), - Type::PropertyInstance(property) if name == "__set__" => Symbol::bound( + Type::PropertyInstance(property) if name == "__set__" => Place::bound( Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(property)), ) .into(), - Type::StringLiteral(literal) if name == "startswith" => Symbol::bound( + Type::StringLiteral(literal) if name == "startswith" => Place::bound( Type::MethodWrapper(MethodWrapperKind::StrStartswith(literal)), ) .into(), @@ -3087,7 +3138,7 @@ impl<'db> Type<'db> { Type::ClassLiteral(class) if name == "__get__" && class.is_known(db, KnownClass::FunctionType) => { - Symbol::bound(Type::WrapperDescriptor( + Place::bound(Type::WrapperDescriptor( WrapperDescriptorKind::FunctionTypeDunderGet, )) .into() @@ -3095,7 +3146,7 @@ impl<'db> Type<'db> { Type::ClassLiteral(class) if name == "__get__" && class.is_known(db, KnownClass::Property) => { - Symbol::bound(Type::WrapperDescriptor( + Place::bound(Type::WrapperDescriptor( WrapperDescriptorKind::PropertyDunderGet, )) .into() @@ -3103,16 +3154,14 @@ impl<'db> Type<'db> { Type::ClassLiteral(class) if name == "__set__" && class.is_known(db, KnownClass::Property) => { - Symbol::bound(Type::WrapperDescriptor( + Place::bound(Type::WrapperDescriptor( WrapperDescriptorKind::PropertyDunderSet, )) .into() } Type::BoundMethod(bound_method) => match name_str { - "__self__" => Symbol::bound(bound_method.self_instance(db)).into(), - "__func__" => { - Symbol::bound(Type::FunctionLiteral(bound_method.function(db))).into() - } + "__self__" => Place::bound(bound_method.self_instance(db)).into(), + "__func__" => Place::bound(Type::FunctionLiteral(bound_method.function(db))).into(), _ => { KnownClass::MethodType .to_instance(db) @@ -3136,7 +3185,7 @@ impl<'db> Type<'db> { .member_lookup_with_policy(db, name, policy), Type::Callable(_) | Type::DataclassTransformer(_) if name_str == "__call__" => { - Symbol::bound(self).into() + Place::bound(self).into() } Type::Callable(callable) if callable.is_function_like(db) => KnownClass::FunctionType @@ -3157,22 +3206,22 @@ impl<'db> Type<'db> { } else { python_version.minor }; - Symbol::bound(Type::IntLiteral(segment.into())).into() + Place::bound(Type::IntLiteral(segment.into())).into() } Type::PropertyInstance(property) if name == "fget" => { - Symbol::bound(property.getter(db).unwrap_or(Type::none(db))).into() + Place::bound(property.getter(db).unwrap_or(Type::none(db))).into() } Type::PropertyInstance(property) if name == "fset" => { - Symbol::bound(property.setter(db).unwrap_or(Type::none(db))).into() + Place::bound(property.setter(db).unwrap_or(Type::none(db))).into() } Type::IntLiteral(_) if matches!(name_str, "real" | "numerator") => { - Symbol::bound(self).into() + Place::bound(self).into() } Type::BooleanLiteral(bool_value) if matches!(name_str, "real" | "numerator") => { - Symbol::bound(Type::IntLiteral(i64::from(bool_value))).into() + Place::bound(Type::IntLiteral(i64::from(bool_value))).into() } Type::ModuleLiteral(module) => module.static_member(db, name_str).into(), @@ -3184,7 +3233,7 @@ impl<'db> Type<'db> { _ if policy.no_instance_fallback() => self.invoke_descriptor_protocol( db, name_str, - Symbol::Unbound.into(), + Place::Unbound.into(), InstanceFallbackShadowsNonDataDescriptor::No, policy, ), @@ -3225,7 +3274,7 @@ impl<'db> Type<'db> { .and_then(|instance| instance.class.known(db)), Some(KnownClass::ModuleType | KnownClass::GenericAlias) ) { - return Symbol::Unbound.into(); + return Place::Unbound.into(); } self.try_call_dunder( @@ -3235,16 +3284,16 @@ impl<'db> Type<'db> { StringLiteralType::new(db, Box::from(name.as_str())), )]), ) - .map(|outcome| Symbol::bound(outcome.return_type(db))) + .map(|outcome| Place::bound(outcome.return_type(db))) // TODO: Handle call errors here. - .unwrap_or(Symbol::Unbound) + .unwrap_or(Place::Unbound) .into() }; let custom_getattribute_result = || { // Avoid cycles when looking up `__getattribute__` if "__getattribute__" == name.as_str() { - return Symbol::Unbound.into(); + return Place::Unbound.into(); } // Typeshed has a `__getattribute__` method defined on `builtins.object` so we @@ -3257,25 +3306,25 @@ impl<'db> Type<'db> { )]), MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, ) - .map(|outcome| Symbol::bound(outcome.return_type(db))) + .map(|outcome| Place::bound(outcome.return_type(db))) // TODO: Handle call errors here. - .unwrap_or(Symbol::Unbound) + .unwrap_or(Place::Unbound) .into() }; match result { - member @ SymbolAndQualifiers { - symbol: Symbol::Type(_, Boundness::Bound), + member @ PlaceAndQualifiers { + place: Place::Type(_, Boundness::Bound), qualifiers: _, } => member, - member @ SymbolAndQualifiers { - symbol: Symbol::Type(_, Boundness::PossiblyUnbound), + member @ PlaceAndQualifiers { + place: Place::Type(_, Boundness::PossiblyUnbound), qualifiers: _, } => member .or_fall_back_to(db, custom_getattribute_result) .or_fall_back_to(db, custom_getattr_result), - SymbolAndQualifiers { - symbol: Symbol::Unbound, + PlaceAndQualifiers { + place: Place::Unbound, qualifiers: _, } => custom_getattribute_result().or_fall_back_to(db, custom_getattr_result), } @@ -3291,7 +3340,7 @@ impl<'db> Type<'db> { } if self.is_subtype_of(db, KnownClass::Enum.to_subclass_of(db)) { - return SymbolAndQualifiers::todo("Attribute access on enum classes"); + return PlaceAndQualifiers::todo("Attribute access on enum classes"); } let class_attr_fallback = Self::try_call_dunder_get_on_attribute( @@ -4366,9 +4415,9 @@ impl<'db> Type<'db> { Name::new_static("__call__"), MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) - .symbol + .place { - Symbol::Type(dunder_callable, boundness) => { + Place::Type(dunder_callable, boundness) => { let mut bindings = dunder_callable.bindings(db); bindings.replace_callable_type(dunder_callable, self); if boundness == Boundness::PossiblyUnbound { @@ -4376,7 +4425,7 @@ impl<'db> Type<'db> { } bindings } - Symbol::Unbound => CallableBinding::not_callable(self).into(), + Place::Unbound => CallableBinding::not_callable(self).into(), } } @@ -4478,9 +4527,9 @@ impl<'db> Type<'db> { name.into(), policy | MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) - .symbol + .place { - Symbol::Type(dunder_callable, boundness) => { + Place::Type(dunder_callable, boundness) => { let bindings = dunder_callable .bindings(db) .match_parameters(argument_types) @@ -4490,7 +4539,7 @@ impl<'db> Type<'db> { } Ok(bindings) } - Symbol::Unbound => Err(CallDunderError::MethodNotAvailable), + Place::Unbound => Err(CallDunderError::MethodNotAvailable), } } @@ -4722,8 +4771,8 @@ impl<'db> Type<'db> { | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, ); let new_call_outcome = new_method.and_then(|new_method| { - match new_method.symbol.try_call_dunder_get(db, self_type) { - Symbol::Type(new_method, boundness) => { + match new_method.place.try_call_dunder_get(db, self_type) { + Place::Type(new_method, boundness) => { let result = new_method.try_call(db, argument_types.with_self(Some(self_type)).as_ref()); if boundness == Boundness::PossiblyUnbound { @@ -4732,7 +4781,7 @@ impl<'db> Type<'db> { Some(result.map_err(DunderNewCallError::CallError)) } } - Symbol::Unbound => None, + Place::Unbound => None, } }); @@ -4751,7 +4800,7 @@ impl<'db> Type<'db> { MemberLookupPolicy::NO_INSTANCE_FALLBACK | MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, ) - .symbol + .place .is_unbound() { Some(init_ty.try_call_dunder(db, "__init__", argument_types)) @@ -5024,9 +5073,9 @@ impl<'db> Type<'db> { ], }); }; - let instance = Type::ClassLiteral(class) - .to_instance(db) - .expect("enclosing_class_symbol must return type that can be instantiated"); + let instance = Type::ClassLiteral(class).to_instance(db).expect( + "nearest_enclosing_class must return type that can be instantiated", + ); Ok(Type::TypeVar(TypeVarInstance::new( db, ast::name::Name::new("Self"), @@ -5685,6 +5734,20 @@ impl<'db> Type<'db> { _ => None, } } + + pub(crate) fn generic_origin(self, db: &'db dyn Db) -> Option> { + match self { + Type::GenericAlias(generic) => Some(generic.origin(db)), + Type::NominalInstance(instance) => { + if let ClassType::Generic(generic) = instance.class { + Some(generic.origin(db)) + } else { + None + } + } + _ => None, + } + } } impl<'db> From<&Type<'db>> for Type<'db> { @@ -5902,7 +5965,7 @@ bitflags! { /// When inferring the type of an annotation expression, we can also encounter type qualifiers /// such as `ClassVar` or `Final`. These do not affect the inferred type itself, but rather -/// control how a particular symbol can be accessed or modified. This struct holds a type and +/// control how a particular place can be accessed or modified. This struct holds a type and /// a set of type qualifiers. /// /// Example: `Annotated[ClassVar[tuple[int]], "metadata"]` would have type `tuple[int]` and the @@ -6073,7 +6136,7 @@ impl<'db> InvalidTypeExpression<'db> { }; let Some(module_member_with_same_name) = ty .member(db, module_name_final_part) - .symbol + .place .ignore_possibly_unbound() else { return; @@ -7001,6 +7064,14 @@ impl Truthiness { } } + pub(crate) fn or(self, other: Self) -> Self { + match (self, other) { + (Truthiness::AlwaysFalse, Truthiness::AlwaysFalse) => Truthiness::AlwaysFalse, + (Truthiness::AlwaysTrue, _) | (_, Truthiness::AlwaysTrue) => Truthiness::AlwaysTrue, + _ => Truthiness::Ambiguous, + } + } + fn into_type(self, db: &dyn Db) -> Type { match self { Self::AlwaysTrue => Type::BooleanLiteral(true), @@ -7450,7 +7521,7 @@ pub struct ModuleLiteralType<'db> { } impl<'db> ModuleLiteralType<'db> { - fn static_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { + fn static_member(self, db: &'db dyn Db, name: &str) -> Place<'db> { // `__dict__` is a very special member that is never overridden by module globals; // we should always look it up directly as an attribute on `types.ModuleType`, // never in the global scope of the module. @@ -7458,7 +7529,7 @@ impl<'db> ModuleLiteralType<'db> { return KnownClass::ModuleType .to_instance(db) .member(db, "__dict__") - .symbol; + .place; } // If the file that originally imported the module has also imported a submodule @@ -7477,7 +7548,7 @@ impl<'db> ModuleLiteralType<'db> { full_submodule_name.extend(&submodule_name); if imported_submodules.contains(&full_submodule_name) { if let Some(submodule) = resolve_module(db, &full_submodule_name) { - return Symbol::bound(Type::module_literal(db, importing_file, &submodule)); + return Place::bound(Type::module_literal(db, importing_file, &submodule)); } } } @@ -7486,7 +7557,7 @@ impl<'db> ModuleLiteralType<'db> { .file() .map(|file| imported_symbol(db, file, name, None)) .unwrap_or_default() - .symbol + .place } } @@ -7638,8 +7709,8 @@ impl<'db> UnionType<'db> { pub(crate) fn map_with_boundness( self, db: &'db dyn Db, - mut transform_fn: impl FnMut(&Type<'db>) -> Symbol<'db>, - ) -> Symbol<'db> { + mut transform_fn: impl FnMut(&Type<'db>) -> Place<'db>, + ) -> Place<'db> { let mut builder = UnionBuilder::new(db); let mut all_unbound = true; @@ -7647,10 +7718,10 @@ impl<'db> UnionType<'db> { for ty in self.elements(db) { let ty_member = transform_fn(ty); match ty_member { - Symbol::Unbound => { + Place::Unbound => { possibly_unbound = true; } - Symbol::Type(ty_member, member_boundness) => { + Place::Type(ty_member, member_boundness) => { if member_boundness == Boundness::PossiblyUnbound { possibly_unbound = true; } @@ -7662,9 +7733,9 @@ impl<'db> UnionType<'db> { } if all_unbound { - Symbol::Unbound + Place::Unbound } else { - Symbol::Type( + Place::Type( builder.build(), if possibly_unbound { Boundness::PossiblyUnbound @@ -7678,24 +7749,24 @@ impl<'db> UnionType<'db> { pub(crate) fn map_with_boundness_and_qualifiers( self, db: &'db dyn Db, - mut transform_fn: impl FnMut(&Type<'db>) -> SymbolAndQualifiers<'db>, - ) -> SymbolAndQualifiers<'db> { + mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, + ) -> PlaceAndQualifiers<'db> { let mut builder = UnionBuilder::new(db); let mut qualifiers = TypeQualifiers::empty(); let mut all_unbound = true; let mut possibly_unbound = false; for ty in self.elements(db) { - let SymbolAndQualifiers { - symbol: ty_member, + let PlaceAndQualifiers { + place: ty_member, qualifiers: new_qualifiers, } = transform_fn(ty); qualifiers |= new_qualifiers; match ty_member { - Symbol::Unbound => { + Place::Unbound => { possibly_unbound = true; } - Symbol::Type(ty_member, member_boundness) => { + Place::Type(ty_member, member_boundness) => { if member_boundness == Boundness::PossiblyUnbound { possibly_unbound = true; } @@ -7705,11 +7776,11 @@ impl<'db> UnionType<'db> { } } } - SymbolAndQualifiers { - symbol: if all_unbound { - Symbol::Unbound + PlaceAndQualifiers { + place: if all_unbound { + Place::Unbound } else { - Symbol::Type( + Place::Type( builder.build(), if possibly_unbound { Boundness::PossiblyUnbound @@ -7950,10 +8021,10 @@ impl<'db> IntersectionType<'db> { pub(crate) fn map_with_boundness( self, db: &'db dyn Db, - mut transform_fn: impl FnMut(&Type<'db>) -> Symbol<'db>, - ) -> Symbol<'db> { + mut transform_fn: impl FnMut(&Type<'db>) -> Place<'db>, + ) -> Place<'db> { if !self.negative(db).is_empty() { - return Symbol::todo("map_with_boundness: intersections with negative contributions"); + return Place::todo("map_with_boundness: intersections with negative contributions"); } let mut builder = IntersectionBuilder::new(db); @@ -7963,8 +8034,8 @@ impl<'db> IntersectionType<'db> { for ty in self.positive(db) { let ty_member = transform_fn(ty); match ty_member { - Symbol::Unbound => {} - Symbol::Type(ty_member, member_boundness) => { + Place::Unbound => {} + Place::Type(ty_member, member_boundness) => { all_unbound = false; if member_boundness == Boundness::Bound { any_definitely_bound = true; @@ -7976,9 +8047,9 @@ impl<'db> IntersectionType<'db> { } if all_unbound { - Symbol::Unbound + Place::Unbound } else { - Symbol::Type( + Place::Type( builder.build(), if any_definitely_bound { Boundness::Bound @@ -7992,10 +8063,10 @@ impl<'db> IntersectionType<'db> { pub(crate) fn map_with_boundness_and_qualifiers( self, db: &'db dyn Db, - mut transform_fn: impl FnMut(&Type<'db>) -> SymbolAndQualifiers<'db>, - ) -> SymbolAndQualifiers<'db> { + mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, + ) -> PlaceAndQualifiers<'db> { if !self.negative(db).is_empty() { - return Symbol::todo("map_with_boundness: intersections with negative contributions") + return Place::todo("map_with_boundness: intersections with negative contributions") .into(); } @@ -8005,16 +8076,16 @@ impl<'db> IntersectionType<'db> { let mut any_unbound = false; let mut any_possibly_unbound = false; for ty in self.positive(db) { - let SymbolAndQualifiers { - symbol: member, + let PlaceAndQualifiers { + place: member, qualifiers: new_qualifiers, } = transform_fn(ty); qualifiers |= new_qualifiers; match member { - Symbol::Unbound => { + Place::Unbound => { any_unbound = true; } - Symbol::Type(ty_member, member_boundness) => { + Place::Type(ty_member, member_boundness) => { if member_boundness == Boundness::PossiblyUnbound { any_possibly_unbound = true; } @@ -8024,11 +8095,11 @@ impl<'db> IntersectionType<'db> { } } - SymbolAndQualifiers { - symbol: if any_unbound { - Symbol::Unbound + PlaceAndQualifiers { + place: if any_unbound { + Place::Unbound } else { - Symbol::Type( + Place::Type( builder.build(), if any_possibly_unbound { Boundness::PossiblyUnbound @@ -8400,8 +8471,8 @@ impl<'db> BoundSuperType<'db> { fn try_call_dunder_get_on_attribute( self, db: &'db dyn Db, - attribute: SymbolAndQualifiers<'db>, - ) -> Option> { + attribute: PlaceAndQualifiers<'db>, + ) -> Option> { let owner = self.owner(db); match owner { @@ -8436,7 +8507,7 @@ impl<'db> BoundSuperType<'db> { db: &'db dyn Db, name: &str, policy: MemberLookupPolicy, - ) -> SymbolAndQualifiers<'db> { + ) -> PlaceAndQualifiers<'db> { let owner = self.owner(db); let class = match owner { SuperOwnerKind::Dynamic(_) => { @@ -8461,7 +8532,7 @@ impl<'db> BoundSuperType<'db> { // super(B[int], b_unknown) // ``` match class_literal.generic_context(db) { - Some(_) => Symbol::bound(todo_type!("super in generic class")).into(), + Some(_) => Place::bound(todo_type!("super in generic class")).into(), None => class_literal.class_member_from_mro( db, name, @@ -8489,7 +8560,7 @@ static_assertions::assert_eq_size!(Type, [u8; 16]); pub(crate) mod tests { use super::*; use crate::db::tests::{TestDbBuilder, setup_db}; - use crate::symbol::{global_symbol, typing_extensions_symbol, typing_symbol}; + use crate::place::{global_symbol, typing_extensions_symbol, typing_symbol}; use ruff_db::files::system_path_to_file; use ruff_db::parsed::parsed_module; use ruff_db::system::DbWithWritableSystem as _; @@ -8520,9 +8591,9 @@ pub(crate) mod tests { .build() .unwrap(); - let typing_no_default = typing_symbol(&db, "NoDefault").symbol.expect_type(); + let typing_no_default = typing_symbol(&db, "NoDefault").place.expect_type(); let typing_extensions_no_default = typing_extensions_symbol(&db, "NoDefault") - .symbol + .place .expect_type(); assert_eq!(typing_no_default.display(&db).to_string(), "NoDefault"); @@ -8555,7 +8626,7 @@ pub(crate) mod tests { )?; let bar = system_path_to_file(&db, "src/bar.py")?; - let a = global_symbol(&db, bar, "a").symbol; + let a = global_symbol(&db, bar, "a").place; assert_eq!( a.expect_type(), @@ -8574,7 +8645,7 @@ pub(crate) mod tests { )?; db.clear_salsa_events(); - let a = global_symbol(&db, bar, "a").symbol; + let a = global_symbol(&db, bar, "a").place; assert_eq!( a.expect_type(), diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 3d349abf17f09..14906fd6daa70 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -12,7 +12,7 @@ use super::{ }; use crate::db::Db; use crate::dunder_all::dunder_all_names; -use crate::symbol::{Boundness, Symbol}; +use crate::place::{Boundness, Place}; use crate::types::diagnostic::{ CALL_NON_CALLABLE, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS, @@ -770,7 +770,7 @@ impl<'db> Bindings<'db> { // TODO: we could emit a diagnostic here (if default is not set) overload.set_return_type( match instance_ty.static_member(db, attr_name.value(db)) { - Symbol::Type(ty, Boundness::Bound) => { + Place::Type(ty, Boundness::Bound) => { if instance_ty.is_fully_static(db) { ty } else { @@ -782,10 +782,10 @@ impl<'db> Bindings<'db> { union_with_default(ty) } } - Symbol::Type(ty, Boundness::PossiblyUnbound) => { + Place::Type(ty, Boundness::PossiblyUnbound) => { union_with_default(ty) } - Symbol::Unbound => default, + Place::Unbound => default, }, ); } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 205e587fad594..ffc558e969724 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -7,7 +7,7 @@ use super::{ infer_unpack_types, }; use crate::semantic_index::DeclarationWithConstraint; -use crate::semantic_index::definition::Definition; +use crate::semantic_index::definition::{Definition, DefinitionState}; use crate::types::function::{DataclassTransformerParams, KnownFunction}; use crate::types::generics::{GenericContext, Specialization}; use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature}; @@ -17,17 +17,16 @@ use crate::types::{ use crate::{ Db, FxOrderSet, KnownModule, Program, module_resolver::file_to_module, + place::{ + Boundness, LookupError, LookupResult, Place, PlaceAndQualifiers, class_symbol, + known_module_symbol, place_from_bindings, place_from_declarations, + }, semantic_index::{ ast_ids::HasScopedExpressionId, attribute_assignments, definition::{DefinitionKind, TargetKind}, - semantic_index, - symbol::ScopeId, - symbol_table, use_def_map, - }, - symbol::{ - Boundness, LookupError, LookupResult, Symbol, SymbolAndQualifiers, class_symbol, - known_module_symbol, symbol_from_bindings, symbol_from_declarations, + place::ScopeId, + place_table, semantic_index, use_def_map, }, types::{ CallArgumentTypes, CallError, CallErrorKind, DynamicType, MetaclassCandidate, TupleType, @@ -454,7 +453,7 @@ impl<'db> ClassType<'db> { db: &'db dyn Db, name: &str, policy: MemberLookupPolicy, - ) -> SymbolAndQualifiers<'db> { + ) -> PlaceAndQualifiers<'db> { let (class_literal, specialization) = self.class_literal(db); class_literal.class_member_inner(db, specialization, name, policy) } @@ -462,10 +461,10 @@ impl<'db> ClassType<'db> { /// Returns the inferred type of the class member named `name`. Only bound members /// or those marked as ClassVars are considered. /// - /// Returns [`Symbol::Unbound`] if `name` cannot be found in this class's scope + /// Returns [`Place::Unbound`] if `name` cannot be found in this class's scope /// directly. Use [`ClassType::class_member`] if you require a method that will /// traverse through the MRO until it finds the member. - pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { + pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { let (class_literal, specialization) = self.class_literal(db); class_literal .own_class_member(db, specialization, name) @@ -475,7 +474,7 @@ impl<'db> ClassType<'db> { /// Look up an instance attribute (available in `__dict__`) of the given name. /// /// See [`Type::instance_member`] for more details. - pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { + pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { let (class_literal, specialization) = self.class_literal(db); class_literal .instance_member(db, specialization, name) @@ -484,7 +483,7 @@ impl<'db> ClassType<'db> { /// A helper function for `instance_member` that looks up the `name` attribute only on /// this class, not on its superclasses. - fn own_instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { + fn own_instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { let (class_literal, specialization) = self.class_literal(db); class_literal .own_instance_member(db, name) @@ -502,9 +501,9 @@ impl<'db> ClassType<'db> { MemberLookupPolicy::NO_INSTANCE_FALLBACK | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, ) - .symbol; + .place; - if let Symbol::Type(Type::BoundMethod(metaclass_dunder_call_function), _) = + if let Place::Type(Type::BoundMethod(metaclass_dunder_call_function), _) = metaclass_dunder_call_function_symbol { // TODO: this intentionally diverges from step 1 in @@ -520,10 +519,10 @@ impl<'db> ClassType<'db> { "__new__".into(), MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, ) - .symbol; + .place; let dunder_new_function = - if let Symbol::Type(Type::FunctionLiteral(dunder_new_function), _) = + if let Place::Type(Type::FunctionLiteral(dunder_new_function), _) = dunder_new_function_symbol { // Step 3: If the return type of the `__new__` evaluates to a type that is not a subclass of this class, @@ -562,7 +561,7 @@ impl<'db> ClassType<'db> { MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, ) - .symbol; + .place; let correct_return_type = self_ty.to_instance(db).unwrap_or_else(Type::unknown); @@ -570,7 +569,7 @@ impl<'db> ClassType<'db> { // same parameters as the `__init__` method after it is bound, and with the return type of // the concrete type of `Self`. let synthesized_dunder_init_callable = - if let Symbol::Type(Type::FunctionLiteral(dunder_init_function), _) = + if let Place::Type(Type::FunctionLiteral(dunder_init_function), _) = dunder_init_function_symbol { let synthesized_signature = |signature: Signature<'db>| { @@ -612,9 +611,9 @@ impl<'db> ClassType<'db> { "__new__".into(), MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, ) - .symbol; + .place; - if let Symbol::Type(Type::FunctionLiteral(new_function), _) = new_function_symbol { + if let Place::Type(Type::FunctionLiteral(new_function), _) = new_function_symbol { new_function.into_bound_method_type(db, self_ty) } else { // Fallback if no `object.__new__` is found. @@ -1136,7 +1135,7 @@ impl<'db> ClassLiteral<'db> { db: &'db dyn Db, name: &str, policy: MemberLookupPolicy, - ) -> SymbolAndQualifiers<'db> { + ) -> PlaceAndQualifiers<'db> { self.class_member_inner(db, None, name, policy) } @@ -1146,10 +1145,10 @@ impl<'db> ClassLiteral<'db> { specialization: Option>, name: &str, policy: MemberLookupPolicy, - ) -> SymbolAndQualifiers<'db> { + ) -> PlaceAndQualifiers<'db> { if name == "__mro__" { let tuple_elements = self.iter_mro(db, specialization).map(Type::from); - return Symbol::bound(TupleType::from_elements(db, tuple_elements)).into(); + return Place::bound(TupleType::from_elements(db, tuple_elements)).into(); } self.class_member_from_mro(db, name, policy, self.iter_mro(db, specialization)) @@ -1161,7 +1160,7 @@ impl<'db> ClassLiteral<'db> { name: &str, policy: MemberLookupPolicy, mro_iter: impl Iterator>, - ) -> SymbolAndQualifiers<'db> { + ) -> PlaceAndQualifiers<'db> { // If we encounter a dynamic type in this class's MRO, we'll save that dynamic type // in this variable. After we've traversed the MRO, we'll either: // (1) Use that dynamic type as the type for this attribute, @@ -1208,18 +1207,18 @@ impl<'db> ClassLiteral<'db> { } match ( - SymbolAndQualifiers::from(lookup_result), + PlaceAndQualifiers::from(lookup_result), dynamic_type_to_intersect_with, ) { (symbol_and_qualifiers, None) => symbol_and_qualifiers, ( - SymbolAndQualifiers { - symbol: Symbol::Type(ty, _), + PlaceAndQualifiers { + place: Place::Type(ty, _), qualifiers, }, Some(dynamic_type), - ) => Symbol::bound( + ) => Place::bound( IntersectionBuilder::new(db) .add_positive(ty) .add_positive(dynamic_type) @@ -1228,19 +1227,19 @@ impl<'db> ClassLiteral<'db> { .with_qualifiers(qualifiers), ( - SymbolAndQualifiers { - symbol: Symbol::Unbound, + PlaceAndQualifiers { + place: Place::Unbound, qualifiers, }, Some(dynamic_type), - ) => Symbol::bound(dynamic_type).with_qualifiers(qualifiers), + ) => Place::bound(dynamic_type).with_qualifiers(qualifiers), } } /// Returns the inferred type of the class member named `name`. Only bound members /// or those marked as ClassVars are considered. /// - /// Returns [`Symbol::Unbound`] if `name` cannot be found in this class's scope + /// Returns [`Place::Unbound`] if `name` cannot be found in this class's scope /// directly. Use [`ClassLiteral::class_member`] if you require a method that will /// traverse through the MRO until it finds the member. pub(super) fn own_class_member( @@ -1248,10 +1247,10 @@ impl<'db> ClassLiteral<'db> { db: &'db dyn Db, specialization: Option>, name: &str, - ) -> SymbolAndQualifiers<'db> { + ) -> PlaceAndQualifiers<'db> { if name == "__dataclass_fields__" && self.dataclass_params(db).is_some() { // Make this class look like a subclass of the `DataClassInstance` protocol - return Symbol::bound(KnownClass::Dict.to_specialized_instance( + return Place::bound(KnownClass::Dict.to_specialized_instance( db, [ KnownClass::Str.to_instance(db), @@ -1287,10 +1286,10 @@ impl<'db> ClassLiteral<'db> { } }); - if symbol.symbol.is_unbound() { + if symbol.place.is_unbound() { if let Some(synthesized_member) = self.own_synthesized_member(db, specialization, name) { - return Symbol::bound(synthesized_member).into(); + return Place::bound(synthesized_member).into(); } } @@ -1322,7 +1321,7 @@ impl<'db> ClassLiteral<'db> { // itself in this case, so we skip the special descriptor handling. if attr_ty.is_fully_static(db) { let dunder_set = attr_ty.class_member(db, "__set__".into()); - if let Some(dunder_set) = dunder_set.symbol.ignore_possibly_unbound() { + if let Some(dunder_set) = dunder_set.place.ignore_possibly_unbound() { // This type of this attribute is a data descriptor. Instead of overwriting the // descriptor attribute, data-classes will (implicitly) call the `__set__` method // of the descriptor. This means that the synthesized `__init__` parameter for @@ -1428,7 +1427,7 @@ impl<'db> ClassLiteral<'db> { .to_class_literal(db) .into_class_literal()? .own_class_member(db, None, name) - .symbol + .place .ignore_possibly_unbound() } _ => None, @@ -1490,10 +1489,10 @@ impl<'db> ClassLiteral<'db> { let mut attributes = FxOrderMap::default(); let class_body_scope = self.body_scope(db); - let table = symbol_table(db, class_body_scope); + let table = place_table(db, class_body_scope); let use_def = use_def_map(db, class_body_scope); - for (symbol_id, declarations) in use_def.all_public_declarations() { + for (place_id, declarations) in use_def.all_public_declarations() { // Here, we exclude all declarations that are not annotated assignments. We need this because // things like function definitions and nested classes would otherwise be considered dataclass // fields. The check is too broad in the sense that it also excludes (weird) constructs where @@ -1504,7 +1503,7 @@ impl<'db> ClassLiteral<'db> { if !declarations .clone() .all(|DeclarationWithConstraint { declaration, .. }| { - declaration.is_some_and(|declaration| { + declaration.is_defined_and(|declaration| { matches!( declaration.kind(db), DefinitionKind::AnnotatedAssignment(..) @@ -1515,18 +1514,18 @@ impl<'db> ClassLiteral<'db> { continue; } - let symbol = table.symbol(symbol_id); + let place_expr = table.place_expr(place_id); - if let Ok(attr) = symbol_from_declarations(db, declarations) { + if let Ok(attr) = place_from_declarations(db, declarations) { if attr.is_class_var() { continue; } - if let Some(attr_ty) = attr.symbol.ignore_possibly_unbound() { - let bindings = use_def.public_bindings(symbol_id); - let default_ty = symbol_from_bindings(db, bindings).ignore_possibly_unbound(); + if let Some(attr_ty) = attr.place.ignore_possibly_unbound() { + let bindings = use_def.public_bindings(place_id); + let default_ty = place_from_bindings(db, bindings).ignore_possibly_unbound(); - attributes.insert(symbol.name().clone(), (attr_ty, default_ty)); + attributes.insert(place_expr.expect_name().clone(), (attr_ty, default_ty)); } } } @@ -1542,7 +1541,7 @@ impl<'db> ClassLiteral<'db> { db: &'db dyn Db, specialization: Option>, name: &str, - ) -> SymbolAndQualifiers<'db> { + ) -> PlaceAndQualifiers<'db> { let mut union = UnionBuilder::new(db); let mut union_qualifiers = TypeQualifiers::empty(); @@ -1552,13 +1551,13 @@ impl<'db> ClassLiteral<'db> { // Skip over these very special class bases that aren't really classes. } ClassBase::Dynamic(_) => { - return SymbolAndQualifiers::todo( + return PlaceAndQualifiers::todo( "instance attribute on class with dynamic base", ); } ClassBase::Class(class) => { - if let member @ SymbolAndQualifiers { - symbol: Symbol::Type(ty, boundness), + if let member @ PlaceAndQualifiers { + place: Place::Type(ty, boundness), qualifiers, } = class.own_instance_member(db, name) { @@ -1571,7 +1570,7 @@ impl<'db> ClassLiteral<'db> { return member; } - return Symbol::bound(union.add(ty).build()) + return Place::bound(union.add(ty).build()) .with_qualifiers(union_qualifiers); } @@ -1584,13 +1583,12 @@ impl<'db> ClassLiteral<'db> { } if union.is_empty() { - Symbol::Unbound.with_qualifiers(TypeQualifiers::empty()) + Place::Unbound.with_qualifiers(TypeQualifiers::empty()) } else { - // If we have reached this point, we know that we have only seen possibly-unbound symbols. + // If we have reached this point, we know that we have only seen possibly-unbound places. // This means that the final result is still possibly-unbound. - Symbol::Type(union.build(), Boundness::PossiblyUnbound) - .with_qualifiers(union_qualifiers) + Place::Type(union.build(), Boundness::PossiblyUnbound).with_qualifiers(union_qualifiers) } } @@ -1600,7 +1598,7 @@ impl<'db> ClassLiteral<'db> { db: &'db dyn Db, class_body_scope: ScopeId<'db>, name: &str, - ) -> Symbol<'db> { + ) -> Place<'db> { // If we do not see any declarations of an attribute, neither in the class body nor in // any method, we build a union of `Unknown` with the inferred types of all bindings of // that attribute. We include `Unknown` in that union to account for the fact that the @@ -1612,7 +1610,7 @@ impl<'db> ClassLiteral<'db> { let file = class_body_scope.file(db); let index = semantic_index(db, file); let class_map = use_def_map(db, class_body_scope); - let class_table = symbol_table(db, class_body_scope); + let class_table = place_table(db, class_body_scope); for (attribute_assignments, method_scope_id) in attribute_assignments(db, class_body_scope, name) @@ -1623,11 +1621,11 @@ impl<'db> ClassLiteral<'db> { // The attribute assignment inherits the visibility of the method which contains it let is_method_visible = if let Some(method_def) = method_scope.node(db).as_function() { let method = index.expect_single_definition(method_def); - let method_symbol = class_table.symbol_id_by_name(&method_def.name).unwrap(); + let method_place = class_table.place_id_by_name(&method_def.name).unwrap(); class_map - .public_bindings(method_symbol) + .public_bindings(method_place) .find_map(|bind| { - (bind.binding == Some(method)) + (bind.binding.is_defined_and(|def| def == method)) .then(|| class_map.is_binding_visible(db, &bind)) }) .unwrap_or(Truthiness::AlwaysFalse) @@ -1642,7 +1640,7 @@ impl<'db> ClassLiteral<'db> { let unbound_visibility = attribute_assignments .peek() .map(|attribute_assignment| { - if attribute_assignment.binding.is_none() { + if attribute_assignment.binding.is_undefined() { method_map.is_binding_visible(db, attribute_assignment) } else { Truthiness::AlwaysFalse @@ -1651,7 +1649,7 @@ impl<'db> ClassLiteral<'db> { .unwrap_or(Truthiness::AlwaysFalse); for attribute_assignment in attribute_assignments { - let Some(binding) = attribute_assignment.binding else { + let DefinitionState::Defined(binding) = attribute_assignment.binding else { continue; }; match method_map @@ -1696,10 +1694,10 @@ impl<'db> ClassLiteral<'db> { // TODO: check if there are conflicting declarations match is_attribute_bound { Truthiness::AlwaysTrue => { - return Symbol::bound(annotation_ty); + return Place::bound(annotation_ty); } Truthiness::Ambiguous => { - return Symbol::possibly_unbound(annotation_ty); + return Place::possibly_unbound(annotation_ty); } Truthiness::AlwaysFalse => unreachable!( "If the attribute assignments are all invisible, inference of their types should be skipped" @@ -1722,7 +1720,7 @@ impl<'db> ClassLiteral<'db> { union_of_inferred_types = union_of_inferred_types.add(inferred_ty); } - TargetKind::NameOrAttribute => { + TargetKind::Single => { // We found an un-annotated attribute assignment of the form: // // self.name = @@ -1748,7 +1746,7 @@ impl<'db> ClassLiteral<'db> { union_of_inferred_types = union_of_inferred_types.add(inferred_ty); } - TargetKind::NameOrAttribute => { + TargetKind::Single => { // We found an attribute assignment like: // // for self.name in : @@ -1778,7 +1776,7 @@ impl<'db> ClassLiteral<'db> { union_of_inferred_types = union_of_inferred_types.add(inferred_ty); } - TargetKind::NameOrAttribute => { + TargetKind::Single => { // We found an attribute assignment like: // // with as self.name: @@ -1808,7 +1806,7 @@ impl<'db> ClassLiteral<'db> { union_of_inferred_types = union_of_inferred_types.add(inferred_ty); } - TargetKind::NameOrAttribute => { + TargetKind::Single => { // We found an attribute assignment like: // // [... for self.name in ] @@ -1836,42 +1834,42 @@ impl<'db> ClassLiteral<'db> { } match is_attribute_bound { - Truthiness::AlwaysTrue => Symbol::bound(union_of_inferred_types.build()), - Truthiness::Ambiguous => Symbol::possibly_unbound(union_of_inferred_types.build()), - Truthiness::AlwaysFalse => Symbol::Unbound, + Truthiness::AlwaysTrue => Place::bound(union_of_inferred_types.build()), + Truthiness::Ambiguous => Place::possibly_unbound(union_of_inferred_types.build()), + Truthiness::AlwaysFalse => Place::Unbound, } } /// A helper function for `instance_member` that looks up the `name` attribute only on /// this class, not on its superclasses. - fn own_instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { + fn own_instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { // TODO: There are many things that are not yet implemented here: // - `typing.Final` // - Proper diagnostics let body_scope = self.body_scope(db); - let table = symbol_table(db, body_scope); + let table = place_table(db, body_scope); - if let Some(symbol_id) = table.symbol_id_by_name(name) { + if let Some(place_id) = table.place_id_by_name(name) { let use_def = use_def_map(db, body_scope); - let declarations = use_def.public_declarations(symbol_id); - let declared_and_qualifiers = symbol_from_declarations(db, declarations); + let declarations = use_def.public_declarations(place_id); + let declared_and_qualifiers = place_from_declarations(db, declarations); match declared_and_qualifiers { - Ok(SymbolAndQualifiers { - symbol: mut declared @ Symbol::Type(declared_ty, declaredness), + Ok(PlaceAndQualifiers { + place: mut declared @ Place::Type(declared_ty, declaredness), qualifiers, }) => { // For the purpose of finding instance attributes, ignore `ClassVar` // declarations: if qualifiers.contains(TypeQualifiers::CLASS_VAR) { - declared = Symbol::Unbound; + declared = Place::Unbound; } // The attribute is declared in the class body. - let bindings = use_def.public_bindings(symbol_id); - let inferred = symbol_from_bindings(db, bindings); + let bindings = use_def.public_bindings(place_id); + let inferred = place_from_bindings(db, bindings); let has_binding = !inferred.is_unbound(); if has_binding { @@ -1887,7 +1885,7 @@ impl<'db> ClassLiteral<'db> { // we trust the declared type. declared.with_qualifiers(qualifiers) } else { - Symbol::Type( + Place::Type( UnionType::from_elements(db, [declared_ty, implicit_ty]), declaredness, ) @@ -1900,7 +1898,7 @@ impl<'db> ClassLiteral<'db> { // has a class-level default value, but it would not be // found in a `__dict__` lookup. - Symbol::Unbound.into() + Place::Unbound.into() } } else { // The attribute is declared but not bound in the class body. @@ -1916,7 +1914,7 @@ impl<'db> ClassLiteral<'db> { Self::implicit_instance_attribute(db, body_scope, name) .ignore_possibly_unbound() { - Symbol::Type( + Place::Type( UnionType::from_elements(db, [declared_ty, implicit_ty]), declaredness, ) @@ -1928,8 +1926,8 @@ impl<'db> ClassLiteral<'db> { } } - Ok(SymbolAndQualifiers { - symbol: Symbol::Unbound, + Ok(PlaceAndQualifiers { + place: Place::Unbound, qualifiers: _, }) => { // The attribute is not *declared* in the class body. It could still be declared/bound @@ -1939,7 +1937,7 @@ impl<'db> ClassLiteral<'db> { } Err((declared, _conflicting_declarations)) => { // There are conflicting declarations for this attribute in the class body. - Symbol::bound(declared.inner_type()).with_qualifiers(declared.qualifiers()) + Place::bound(declared.inner_type()).with_qualifiers(declared.qualifiers()) } } } else { @@ -2454,16 +2452,16 @@ impl<'db> KnownClass { self, db: &'db dyn Db, ) -> Result, KnownClassLookupError<'db>> { - let symbol = known_module_symbol(db, self.canonical_module(db), self.name(db)).symbol; + let symbol = known_module_symbol(db, self.canonical_module(db), self.name(db)).place; match symbol { - Symbol::Type(Type::ClassLiteral(class_literal), Boundness::Bound) => Ok(class_literal), - Symbol::Type(Type::ClassLiteral(class_literal), Boundness::PossiblyUnbound) => { + Place::Type(Type::ClassLiteral(class_literal), Boundness::Bound) => Ok(class_literal), + Place::Type(Type::ClassLiteral(class_literal), Boundness::PossiblyUnbound) => { Err(KnownClassLookupError::ClassPossiblyUnbound { class_literal }) } - Symbol::Type(found_type, _) => { + Place::Type(found_type, _) => { Err(KnownClassLookupError::SymbolNotAClass { found_type }) } - Symbol::Unbound => Err(KnownClassLookupError::ClassNotFound), + Place::Unbound => Err(KnownClassLookupError::ClassNotFound), } } diff --git a/crates/ty_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs index f0596b0318a5b..498a1b644a664 100644 --- a/crates/ty_python_semantic/src/types/context.rs +++ b/crates/ty_python_semantic/src/types/context.rs @@ -11,8 +11,8 @@ use ruff_text_size::{Ranged, TextRange}; use super::{Type, TypeCheckDiagnostics, binding_type}; use crate::lint::LintSource; +use crate::semantic_index::place::ScopeId; use crate::semantic_index::semantic_index; -use crate::semantic_index::symbol::ScopeId; use crate::types::function::FunctionDecorators; use crate::{ Db, diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 645a102e84b22..02f38c5701b25 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -794,7 +794,7 @@ mod tests { use crate::Db; use crate::db::tests::setup_db; - use crate::symbol::typing_extensions_symbol; + use crate::place::typing_extensions_symbol; use crate::types::{KnownClass, Parameter, Parameters, Signature, StringLiteralType, Type}; #[test] @@ -833,7 +833,7 @@ mod tests { ); let iterator_synthesized = typing_extensions_symbol(&db, "Iterator") - .symbol + .place .ignore_possibly_unbound() .unwrap() .to_instance(&db) diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 2911fbb1d630a..3b67e9c682fef 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -58,11 +58,11 @@ use ruff_python_ast as ast; use ruff_text_size::Ranged; use crate::module_resolver::{KnownModule, file_to_module}; +use crate::place::{Boundness, Place, place_from_bindings}; use crate::semantic_index::ast_ids::HasScopedUseId; use crate::semantic_index::definition::Definition; +use crate::semantic_index::place::ScopeId; use crate::semantic_index::semantic_index; -use crate::semantic_index::symbol::ScopeId; -use crate::symbol::{Boundness, Symbol, symbol_from_bindings}; use crate::types::generics::GenericContext; use crate::types::narrow::ClassInfoConstraintFunction; use crate::types::signatures::{CallableSignature, Signature}; @@ -234,8 +234,8 @@ impl<'db> OverloadLiteral<'db> { .name .scoped_use_id(db, scope); - let Symbol::Type(Type::FunctionLiteral(previous_type), Boundness::Bound) = - symbol_from_bindings(db, use_def.bindings_at_use(use_id)) + let Place::Type(Type::FunctionLiteral(previous_type), Boundness::Bound) = + place_from_bindings(db, use_def.bindings_at_use(use_id)) else { return None; }; @@ -927,7 +927,7 @@ pub(crate) mod tests { use super::*; use crate::db::tests::setup_db; - use crate::symbol::known_module_symbol; + use crate::place::known_module_symbol; #[test] fn known_function_roundtrip_from_str() { @@ -977,7 +977,7 @@ pub(crate) mod tests { }; let function_definition = known_module_symbol(&db, module, function_name) - .symbol + .place .expect_type() .expect_function_literal() .definition(&db); diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index c8f150aa437e4..13a1ec3487fcd 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -1,9 +1,9 @@ use crate::Db; -use crate::semantic_index::symbol::ScopeId; +use crate::place::{imported_symbol, place_from_bindings, place_from_declarations}; +use crate::semantic_index::place::ScopeId; use crate::semantic_index::{ - attribute_scopes, global_scope, semantic_index, symbol_table, use_def_map, + attribute_scopes, global_scope, place_table, semantic_index, use_def_map, }; -use crate::symbol::{imported_symbol, symbol_from_bindings, symbol_from_declarations}; use crate::types::{ClassBase, ClassLiteral, KnownClass, Type}; use ruff_python_ast::name::Name; use rustc_hash::FxHashSet; @@ -13,28 +13,27 @@ pub(crate) fn all_declarations_and_bindings<'db>( scope_id: ScopeId<'db>, ) -> impl Iterator + 'db { let use_def_map = use_def_map(db, scope_id); - let symbol_table = symbol_table(db, scope_id); + let table = place_table(db, scope_id); use_def_map .all_public_declarations() .filter_map(move |(symbol_id, declarations)| { - if symbol_from_declarations(db, declarations) - .is_ok_and(|result| !result.symbol.is_unbound()) - { - Some(symbol_table.symbol(symbol_id).name().clone()) - } else { - None - } + place_from_declarations(db, declarations) + .ok() + .and_then(|result| { + result + .place + .ignore_possibly_unbound() + .and_then(|_| table.place_expr(symbol_id).as_name().cloned()) + }) }) .chain( use_def_map .all_public_bindings() .filter_map(move |(symbol_id, bindings)| { - if symbol_from_bindings(db, bindings).is_unbound() { - None - } else { - Some(symbol_table.symbol(symbol_id).name().clone()) - } + place_from_bindings(db, bindings) + .ignore_possibly_unbound() + .and_then(|_| table.place_expr(symbol_id).as_name().cloned()) }), ) } @@ -132,16 +131,18 @@ impl AllMembers { let module_scope = global_scope(db, file); let use_def_map = use_def_map(db, module_scope); - let symbol_table = symbol_table(db, module_scope); + let place_table = place_table(db, module_scope); for (symbol_id, _) in use_def_map.all_public_declarations() { - let symbol_name = symbol_table.symbol(symbol_id).name(); + let Some(symbol_name) = place_table.place_expr(symbol_id).as_name() else { + continue; + }; if !imported_symbol(db, file, symbol_name, None) - .symbol + .place .is_unbound() { self.members - .insert(symbol_table.symbol(symbol_id).name().clone()); + .insert(place_table.place_expr(symbol_id).expect_name().clone()); } } } @@ -182,9 +183,10 @@ impl AllMembers { let file = class_body_scope.file(db); let index = semantic_index(db, file); for function_scope_id in attribute_scopes(db, class_body_scope) { - let attribute_table = index.instance_attribute_table(function_scope_id); - for symbol in attribute_table.symbols() { - self.members.insert(symbol.name().clone()); + let place_table = index.place_table(function_scope_id); + for instance_attribute in place_table.instance_attributes() { + let name = instance_attribute.sub_segments()[0].as_member().unwrap(); + self.members.insert(name.clone()); } } } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 5e485b1346fd3..b0e6a19ddda30 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -5,15 +5,15 @@ //! everything in that file's scopes, or give a linter access to types of arbitrary expressions //! (via the [`HasType`](crate::semantic_model::HasType) trait). //! -//! Definition-level inference allows us to look up the types of symbols in other scopes (e.g. for -//! imports) with the minimum inference necessary, so that if we're looking up one symbol from a +//! Definition-level inference allows us to look up the types of places in other scopes (e.g. for +//! imports) with the minimum inference necessary, so that if we're looking up one place from a //! very large module, we can avoid a bunch of unnecessary work. Definition-level inference also //! allows us to handle import cycles without getting into a cycle of scope-level inference //! queries. //! //! The expression-level inference query is needed in only a few cases. Since some assignments can //! have multiple targets (via `x = y = z` or unpacking `(x, y) = z`, they can be associated with -//! multiple definitions (one per assigned symbol). In order to avoid inferring the type of the +//! multiple definitions (one per assigned place). In order to avoid inferring the type of the //! right-hand side once per definition, we infer it as a standalone query, so its result will be //! cached by Salsa. We also need the expression-level query for inferring types in type guard //! expressions (e.g. the test clause of an `if` statement.) @@ -48,7 +48,15 @@ use salsa::plumbing::AsId; use crate::module_name::{ModuleName, ModuleNameResolutionError}; use crate::module_resolver::resolve_module; use crate::node_key::NodeKey; -use crate::semantic_index::ast_ids::{HasScopedExpressionId, HasScopedUseId, ScopedExpressionId}; +use crate::place::{ + Boundness, LookupError, Place, PlaceAndQualifiers, builtins_module_scope, builtins_symbol, + explicit_global_symbol, global_symbol, module_type_implicit_global_declaration, + module_type_implicit_global_symbol, place, place_from_bindings, place_from_declarations, + typing_extensions_symbol, +}; +use crate::semantic_index::ast_ids::{ + HasScopedExpressionId, HasScopedUseId, ScopedExpressionId, ScopedUseId, +}; use crate::semantic_index::definition::{ AnnotatedAssignmentDefinitionKind, AssignmentDefinitionKind, ComprehensionDefinitionKind, Definition, DefinitionKind, DefinitionNodeKey, ExceptHandlerDefinitionKind, @@ -56,15 +64,10 @@ use crate::semantic_index::definition::{ }; use crate::semantic_index::expression::{Expression, ExpressionKind}; use crate::semantic_index::narrowing_constraints::ConstraintKey; -use crate::semantic_index::symbol::{ - FileScopeId, NodeWithScopeKind, NodeWithScopeRef, ScopeId, ScopeKind, ScopedSymbolId, +use crate::semantic_index::place::{ + FileScopeId, NodeWithScopeKind, NodeWithScopeRef, PlaceExpr, ScopeId, ScopeKind, ScopedPlaceId, }; use crate::semantic_index::{EagerSnapshotResult, SemanticIndex, semantic_index}; -use crate::symbol::{ - Boundness, LookupError, builtins_module_scope, builtins_symbol, explicit_global_symbol, - global_symbol, module_type_implicit_global_declaration, module_type_implicit_global_symbol, - symbol, symbol_from_bindings, symbol_from_declarations, typing_extensions_symbol, -}; use crate::types::call::{ Argument, Binding, Bindings, CallArgumentTypes, CallArguments, CallError, }; @@ -93,10 +96,10 @@ use crate::types::{ BareTypeAliasType, CallDunderError, CallableType, ClassLiteral, ClassType, DataclassParams, DynamicType, GenericAlias, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter, - ParameterForm, Parameters, SpecialFormType, StringLiteralType, SubclassOfType, Symbol, - SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers, - TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, - TypeVarVariance, UnionBuilder, UnionType, binding_type, todo_type, + ParameterForm, Parameters, SpecialFormType, StringLiteralType, SubclassOfType, Truthiness, + TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers, + TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypeVarVariance, UnionBuilder, + UnionType, binding_type, todo_type, }; use crate::unpack::{Unpack, UnpackPosition}; use crate::util::subscript::{PyIndex, PySlice}; @@ -154,7 +157,7 @@ fn scope_cycle_initial<'db>(_db: &'db dyn Db, scope: ScopeId<'db>) -> TypeInfere } /// Infer all types for a [`Definition`] (including sub-expressions). -/// Use when resolving a symbol name use or public type of a symbol. +/// Use when resolving a place use or public type of a place. #[salsa::tracked(returns(ref), cycle_fn=definition_cycle_recover, cycle_initial=definition_cycle_initial)] pub(crate) fn infer_definition_types<'db>( db: &'db dyn Db, @@ -1087,10 +1090,10 @@ impl<'db> TypeInferenceBuilder<'db> { /// For (1), this has the consequence of not checking an overloaded function that is being /// shadowed by another function with the same name in this scope. fn check_overloaded_functions(&mut self, scope: &NodeWithScopeKind) { - // Collect all the unique overloaded function symbols in this scope. This requires a set - // because an overloaded function uses the same symbol for each of the overloads and the + // Collect all the unique overloaded function places in this scope. This requires a set + // because an overloaded function uses the same place for each of the overloads and the // implementation. - let overloaded_function_symbols: FxHashSet<_> = self + let overloaded_function_places: FxHashSet<_> = self .types .declarations .iter() @@ -1102,7 +1105,7 @@ impl<'db> TypeInferenceBuilder<'db> { } let function = ty.inner_type().into_function_literal()?; if function.has_known_decorator(self.db(), FunctionDecorators::OVERLOAD) { - Some(definition.symbol(self.db())) + Some(definition.place(self.db())) } else { None } @@ -1115,9 +1118,9 @@ impl<'db> TypeInferenceBuilder<'db> { let mut public_functions = FxHashSet::default(); - for symbol in overloaded_function_symbols { - if let Symbol::Type(Type::FunctionLiteral(function), Boundness::Bound) = - symbol_from_bindings(self.db(), use_def.public_bindings(symbol)) + for place in overloaded_function_places { + if let Place::Type(Type::FunctionLiteral(function), Boundness::Bound) = + place_from_bindings(self.db(), use_def.public_bindings(place)) { if function.file(self.db()) != self.file() { // If the function is not in this file, we don't need to check it. @@ -1442,20 +1445,21 @@ impl<'db> TypeInferenceBuilder<'db> { .is_binding() ); - let file_scope_id = binding.file_scope(self.db()); - let symbol_table = self.index.symbol_table(file_scope_id); + let db = self.db(); + let file_scope_id = binding.file_scope(db); + let place_table = self.index.place_table(file_scope_id); let use_def = self.index.use_def_map(file_scope_id); let mut bound_ty = ty; - let symbol_id = binding.symbol(self.db()); let global_use_def_map = self.index.use_def_map(FileScopeId::global()); - let symbol_name = symbol_table.symbol(symbol_id).name(); - let skip_non_global_scopes = self.skip_non_global_scopes(file_scope_id, symbol_id); + let place_id = binding.place(self.db()); + let expr = place_table.place_expr(place_id); + let skip_non_global_scopes = self.skip_non_global_scopes(file_scope_id, place_id); let declarations = if skip_non_global_scopes { match self .index - .symbol_table(FileScopeId::global()) - .symbol_id_by_name(symbol_name) + .place_table(FileScopeId::global()) + .place_id_by_expr(expr) { Some(id) => global_use_def_map.public_declarations(id), // This case is a syntax error (load before global declaration) but ignore that here @@ -1465,37 +1469,66 @@ impl<'db> TypeInferenceBuilder<'db> { use_def.declarations_at_binding(binding) }; - let declared_ty = symbol_from_declarations(self.db(), declarations) - .and_then(|symbol| { - let symbol = if matches!(symbol.symbol, Symbol::Type(_, Boundness::Bound)) { - symbol + let declared_ty = place_from_declarations(self.db(), declarations) + .and_then(|place| { + Ok(if matches!(place.place, Place::Type(_, Boundness::Bound)) { + place } else if skip_non_global_scopes || self.scope().file_scope_id(self.db()).is_global() { let module_type_declarations = - module_type_implicit_global_declaration(self.db(), symbol_name)?; - symbol.or_fall_back_to(self.db(), || module_type_declarations) + module_type_implicit_global_declaration(self.db(), expr)?; + place.or_fall_back_to(self.db(), || module_type_declarations) } else { - symbol - }; - Ok(symbol) - }) - .map(|SymbolAndQualifiers { symbol, .. }| { - symbol.ignore_possibly_unbound().unwrap_or(Type::unknown()) + place + }) }) + .map( + |PlaceAndQualifiers { + place: resolved_place, + .. + }| { + if resolved_place.is_unbound() && !place_table.place_expr(place_id).is_name() { + if let AnyNodeRef::ExprAttribute(ast::ExprAttribute { + value, attr, .. + }) = node + { + let value_type = self.infer_maybe_standalone_expression(value); + if let Place::Type(ty, Boundness::Bound) = + value_type.member(db, attr).place + { + return ty; + } + } else if let AnyNodeRef::ExprSubscript(ast::ExprSubscript { + value, + slice, + .. + }) = node + { + let value_ty = self.infer_expression(value); + let slice_ty = self.infer_expression(slice); + let result_ty = + self.infer_subscript_expression_types(value, value_ty, slice_ty); + return result_ty; + } + } + resolved_place + .ignore_possibly_unbound() + .unwrap_or(Type::unknown()) + }, + ) .unwrap_or_else(|(ty, conflicting)| { // TODO point out the conflicting declarations in the diagnostic? - let symbol_table = self.index.symbol_table(binding.file_scope(self.db())); - let symbol_name = symbol_table.symbol(binding.symbol(self.db())).name(); + let expr = place_table.place_expr(binding.place(db)); if let Some(builder) = self.context.report_lint(&CONFLICTING_DECLARATIONS, node) { builder.into_diagnostic(format_args!( - "Conflicting declared types for `{symbol_name}`: {}", - conflicting.display(self.db()) + "Conflicting declared types for `{expr}`: {}", + conflicting.display(db) )); } ty.inner_type() }); - if !bound_ty.is_assignable_to(self.db(), declared_ty) { + if !bound_ty.is_assignable_to(db, declared_ty) { report_invalid_assignment(&self.context, node, declared_ty, bound_ty); // allow declarations to override inference in case of invalid assignment bound_ty = declared_ty; @@ -1506,11 +1539,7 @@ impl<'db> TypeInferenceBuilder<'db> { /// Returns `true` if `symbol_id` should be looked up in the global scope, skipping intervening /// local scopes. - fn skip_non_global_scopes( - &self, - file_scope_id: FileScopeId, - symbol_id: ScopedSymbolId, - ) -> bool { + fn skip_non_global_scopes(&self, file_scope_id: FileScopeId, symbol_id: ScopedPlaceId) -> bool { !file_scope_id.is_global() && self .index @@ -1532,24 +1561,20 @@ impl<'db> TypeInferenceBuilder<'db> { let use_def = self.index.use_def_map(declaration.file_scope(self.db())); let prior_bindings = use_def.bindings_at_declaration(declaration); // unbound_ty is Never because for this check we don't care about unbound - let inferred_ty = symbol_from_bindings(self.db(), prior_bindings) + let inferred_ty = place_from_bindings(self.db(), prior_bindings) .with_qualifiers(TypeQualifiers::empty()) .or_fall_back_to(self.db(), || { // Fallback to bindings declared on `types.ModuleType` if it's a global symbol let scope = self.scope().file_scope_id(self.db()); - if scope.is_global() { - module_type_implicit_global_symbol( - self.db(), - self.index - .symbol_table(scope) - .symbol(declaration.symbol(self.db())) - .name(), - ) + let place_table = self.index.place_table(scope); + let expr = place_table.place_expr(declaration.place(self.db())); + if scope.is_global() && expr.is_name() { + module_type_implicit_global_symbol(self.db(), expr.expect_name()) } else { - Symbol::Unbound.into() + Place::Unbound.into() } }) - .symbol + .place .ignore_possibly_unbound() .unwrap_or(Type::Never); let ty = if inferred_ty.is_assignable_to(self.db(), ty.inner_type()) { @@ -1594,12 +1619,12 @@ impl<'db> TypeInferenceBuilder<'db> { } => { let file_scope_id = self.scope().file_scope_id(self.db()); if file_scope_id.is_global() { - let symbol_table = self.index.symbol_table(file_scope_id); - let symbol_name = symbol_table.symbol(definition.symbol(self.db())).name(); + let place_table = self.index.place_table(file_scope_id); + let expr = place_table.place_expr(definition.place(self.db())); if let Some(module_type_implicit_declaration) = - module_type_implicit_global_declaration(self.db(), symbol_name) + module_type_implicit_global_declaration(self.db(), expr) .ok() - .and_then(|sym| sym.symbol.ignore_possibly_unbound()) + .and_then(|place| place.place.ignore_possibly_unbound()) { let declared_type = declared_ty.inner_type(); if !declared_type @@ -1609,11 +1634,11 @@ impl<'db> TypeInferenceBuilder<'db> { self.context.report_lint(&INVALID_DECLARATION, node) { let mut diagnostic = builder.into_diagnostic(format_args!( - "Cannot shadow implicit global attribute `{symbol_name}` with declaration of type `{}`", + "Cannot shadow implicit global attribute `{expr}` with declaration of type `{}`", declared_type.display(self.db()) )); diagnostic.info(format_args!("The global symbol `{}` must always have a type assignable to `{}`", - symbol_name, + expr, module_type_implicit_declaration.display(self.db()) )); } @@ -2550,7 +2575,7 @@ impl<'db> TypeInferenceBuilder<'db> { } unpacked.expression_type(target_ast_id) } - TargetKind::NameOrAttribute => self.infer_context_expression( + TargetKind::Single => self.infer_context_expression( context_expr, context_expr_ty, with_item.is_async(), @@ -3124,7 +3149,7 @@ impl<'db> TypeInferenceBuilder<'db> { }; match object_ty.class_member(db, attribute.into()) { - meta_attr @ SymbolAndQualifiers { .. } if meta_attr.is_class_var() => { + meta_attr @ PlaceAndQualifiers { .. } if meta_attr.is_class_var() => { if emit_diagnostics { if let Some(builder) = self.context.report_lint(&INVALID_ATTRIBUTE_ACCESS, target) @@ -3138,8 +3163,8 @@ impl<'db> TypeInferenceBuilder<'db> { } false } - SymbolAndQualifiers { - symbol: Symbol::Type(meta_attr_ty, meta_attr_boundness), + PlaceAndQualifiers { + place: Place::Type(meta_attr_ty, meta_attr_boundness), qualifiers: _, } => { if is_read_only() { @@ -3155,8 +3180,8 @@ impl<'db> TypeInferenceBuilder<'db> { } false } else { - let assignable_to_meta_attr = if let Symbol::Type(meta_dunder_set, _) = - meta_attr_ty.class_member(db, "__set__".into()).symbol + let assignable_to_meta_attr = if let Place::Type(meta_dunder_set, _) = + meta_attr_ty.class_member(db, "__set__".into()).place { let successful_call = meta_dunder_set .try_call( @@ -3187,13 +3212,12 @@ impl<'db> TypeInferenceBuilder<'db> { ensure_assignable_to(meta_attr_ty) }; - let assignable_to_instance_attribute = - if meta_attr_boundness == Boundness::PossiblyUnbound { - let (assignable, boundness) = if let Symbol::Type( - instance_attr_ty, - instance_attr_boundness, - ) = - object_ty.instance_member(db, attribute).symbol + let assignable_to_instance_attribute = if meta_attr_boundness + == Boundness::PossiblyUnbound + { + let (assignable, boundness) = + if let Place::Type(instance_attr_ty, instance_attr_boundness) = + object_ty.instance_member(db, attribute).place { ( ensure_assignable_to(instance_attr_ty), @@ -3203,30 +3227,30 @@ impl<'db> TypeInferenceBuilder<'db> { (true, Boundness::PossiblyUnbound) }; - if boundness == Boundness::PossiblyUnbound { - report_possibly_unbound_attribute( - &self.context, - target, - attribute, - object_ty, - ); - } + if boundness == Boundness::PossiblyUnbound { + report_possibly_unbound_attribute( + &self.context, + target, + attribute, + object_ty, + ); + } - assignable - } else { - true - }; + assignable + } else { + true + }; assignable_to_meta_attr && assignable_to_instance_attribute } } - SymbolAndQualifiers { - symbol: Symbol::Unbound, + PlaceAndQualifiers { + place: Place::Unbound, .. } => { - if let Symbol::Type(instance_attr_ty, instance_attr_boundness) = - object_ty.instance_member(db, attribute).symbol + if let Place::Type(instance_attr_ty, instance_attr_boundness) = + object_ty.instance_member(db, attribute).place { if instance_attr_boundness == Boundness::PossiblyUnbound { report_possibly_unbound_attribute( @@ -3307,12 +3331,12 @@ impl<'db> TypeInferenceBuilder<'db> { Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => { match object_ty.class_member(db, attribute.into()) { - SymbolAndQualifiers { - symbol: Symbol::Type(meta_attr_ty, meta_attr_boundness), + PlaceAndQualifiers { + place: Place::Type(meta_attr_ty, meta_attr_boundness), qualifiers: _, } => { - let assignable_to_meta_attr = if let Symbol::Type(meta_dunder_set, _) = - meta_attr_ty.class_member(db, "__set__".into()).symbol + let assignable_to_meta_attr = if let Place::Type(meta_dunder_set, _) = + meta_attr_ty.class_member(db, "__set__".into()).place { let successful_call = meta_dunder_set .try_call( @@ -3346,18 +3370,16 @@ impl<'db> TypeInferenceBuilder<'db> { let assignable_to_class_attr = if meta_attr_boundness == Boundness::PossiblyUnbound { - let (assignable, boundness) = if let Symbol::Type( - class_attr_ty, - class_attr_boundness, - ) = object_ty - .find_name_in_mro(db, attribute) - .expect("called on Type::ClassLiteral or Type::SubclassOf") - .symbol - { - (ensure_assignable_to(class_attr_ty), class_attr_boundness) - } else { - (true, Boundness::PossiblyUnbound) - }; + let (assignable, boundness) = + if let Place::Type(class_attr_ty, class_attr_boundness) = object_ty + .find_name_in_mro(db, attribute) + .expect("called on Type::ClassLiteral or Type::SubclassOf") + .place + { + (ensure_assignable_to(class_attr_ty), class_attr_boundness) + } else { + (true, Boundness::PossiblyUnbound) + }; if boundness == Boundness::PossiblyUnbound { report_possibly_unbound_attribute( @@ -3375,14 +3397,14 @@ impl<'db> TypeInferenceBuilder<'db> { assignable_to_meta_attr && assignable_to_class_attr } - SymbolAndQualifiers { - symbol: Symbol::Unbound, + PlaceAndQualifiers { + place: Place::Unbound, .. } => { - if let Symbol::Type(class_attr_ty, class_attr_boundness) = object_ty + if let Place::Type(class_attr_ty, class_attr_boundness) = object_ty .find_name_in_mro(db, attribute) .expect("called on Type::ClassLiteral or Type::SubclassOf") - .symbol + .place { if class_attr_boundness == Boundness::PossiblyUnbound { report_possibly_unbound_attribute( @@ -3399,7 +3421,7 @@ impl<'db> TypeInferenceBuilder<'db> { object_ty.to_instance(self.db()).is_some_and(|instance| { !instance .instance_member(self.db(), attribute) - .symbol + .place .is_unbound() }); @@ -3435,7 +3457,7 @@ impl<'db> TypeInferenceBuilder<'db> { } Type::ModuleLiteral(module) => { - if let Symbol::Type(attr_ty, _) = module.static_member(db, attribute) { + if let Place::Type(attr_ty, _) = module.static_member(db, attribute) { let assignable = value_ty.is_assignable_to(db, attr_ty); if assignable { true @@ -3537,7 +3559,7 @@ impl<'db> TypeInferenceBuilder<'db> { let target_ast_id = target.scoped_expression_id(self.db(), self.scope()); unpacked.expression_type(target_ast_id) } - TargetKind::NameOrAttribute => { + TargetKind::Single => { // `TYPE_CHECKING` is a special variable that should only be assigned `False` // at runtime, but is always considered `True` in type checking. // See mdtest/known_constants.md#user-defined-type_checking for details. @@ -3568,10 +3590,10 @@ impl<'db> TypeInferenceBuilder<'db> { } fn infer_annotated_assignment_statement(&mut self, assignment: &ast::StmtAnnAssign) { - // assignments to non-Names are not Definitions - if matches!(*assignment.target, ast::Expr::Name(_)) { + if assignment.target.is_name_expr() { self.infer_definition(assignment); } else { + // Non-name assignment targets are inferred as ordinary expressions, not definitions. let ast::StmtAnnAssign { range: _, annotation, @@ -3655,10 +3677,10 @@ impl<'db> TypeInferenceBuilder<'db> { } } - // Annotated assignments to non-names are not definitions, so we can only be here - // if the target is a name. In this case, we can simply store types in `target` - // below, instead of calling `infer_expression` (which would return `Never`). - debug_assert!(target.is_name_expr()); + // If the target of an assignment is not one of the place expressions we support, + // then they are not definitions, so we can only be here if the target is in a form supported as a place expression. + // In this case, we can simply store types in `target` below, instead of calling `infer_expression` (which would return `Never`). + debug_assert!(PlaceExpr::try_from(target).is_ok()); if let Some(value) = value { let inferred_ty = self.infer_expression(value); @@ -3701,7 +3723,7 @@ impl<'db> TypeInferenceBuilder<'db> { if assignment.target.is_name_expr() { self.infer_definition(assignment); } else { - // TODO currently we don't consider assignments to non-Names to be Definitions + // Non-name assignment targets are inferred as ordinary expressions, not definitions. self.infer_augment_assignment(assignment); } } @@ -3792,6 +3814,11 @@ impl<'db> TypeInferenceBuilder<'db> { self.store_expression_type(target, previous_value); previous_value } + ast::Expr::Subscript(subscript) => { + let previous_value = self.infer_subscript_load(subscript); + self.store_expression_type(target, previous_value); + previous_value + } _ => self.infer_expression(target), }; let value_type = self.infer_expression(value); @@ -3848,12 +3875,10 @@ impl<'db> TypeInferenceBuilder<'db> { let target_ast_id = target.scoped_expression_id(self.db(), self.scope()); unpacked.expression_type(target_ast_id) } - TargetKind::NameOrAttribute => { - iterable_type.try_iterate(self.db()).unwrap_or_else(|err| { - err.report_diagnostic(&self.context, iterable_type, iterable.into()); - err.fallback_element_type(self.db()) - }) - } + TargetKind::Single => iterable_type.try_iterate(self.db()).unwrap_or_else(|err| { + err.report_diagnostic(&self.context, iterable_type, iterable.into()); + err.fallback_element_type(self.db()) + }), } }; @@ -4147,12 +4172,14 @@ impl<'db> TypeInferenceBuilder<'db> { .map(|star_import| { let symbol_table = self .index - .symbol_table(self.scope().file_scope_id(self.db())); + .place_table(self.scope().file_scope_id(self.db())); (star_import, symbol_table) }); let name = if let Some((star_import, symbol_table)) = star_import_info.as_ref() { - symbol_table.symbol(star_import.symbol_id()).name() + symbol_table + .place_expr(star_import.place_id()) + .expect_name() } else { &alias.name.id }; @@ -4165,7 +4192,7 @@ impl<'db> TypeInferenceBuilder<'db> { // First try loading the requested attribute from the module. if !import_is_self_referential { - if let Symbol::Type(ty, boundness) = module_ty.member(self.db(), name).symbol { + if let Place::Type(ty, boundness) = module_ty.member(self.db(), name).place { if &alias.name != "*" && boundness == Boundness::PossiblyUnbound { // TODO: Consider loading _both_ the attribute and any submodule and unioning them // together if the attribute exists but is possibly-unbound. @@ -4354,15 +4381,22 @@ impl<'db> TypeInferenceBuilder<'db> { #[track_caller] fn infer_expression(&mut self, expression: &ast::Expr) -> Type<'db> { - debug_assert_eq!( - self.index.try_expression(expression), - None, + debug_assert!( + !self.index.is_standalone_expression(expression), "Calling `self.infer_expression` on a standalone-expression is not allowed because it can lead to double-inference. Use `self.infer_standalone_expression` instead." ); self.infer_expression_impl(expression) } + fn infer_maybe_standalone_expression(&mut self, expression: &ast::Expr) -> Type<'db> { + if self.index.is_standalone_expression(expression) { + self.infer_standalone_expression(expression) + } else { + self.infer_expression(expression) + } + } + fn infer_standalone_expression(&mut self, expression: &ast::Expr) -> Type<'db> { let standalone_expression = self.index.expression(expression); let types = infer_expression_types(self.db(), standalone_expression); @@ -4798,7 +4832,7 @@ impl<'db> TypeInferenceBuilder<'db> { // (2) We must *not* call `self.extend()` on the result of the type inference, // because `ScopedExpressionId`s are only meaningful within their own scope, so // we'd add types for random wrong expressions in the current scope - let iterable_type = if comprehension.is_first() { + let iterable_type = if comprehension.is_first() && target.is_name_expr() { let lookup_scope = self .index .parent_scope_id(self.scope().file_scope_id(self.db())) @@ -4806,8 +4840,13 @@ impl<'db> TypeInferenceBuilder<'db> { .to_scope_id(self.db(), self.file()); result.expression_type(iterable.scoped_expression_id(self.db(), lookup_scope)) } else { + let scope = self.types.scope; + self.types.scope = result.scope; self.extend(result); - result.expression_type(iterable.scoped_expression_id(self.db(), self.scope())) + self.types.scope = scope; + result.expression_type( + iterable.scoped_expression_id(self.db(), expression.scope(self.db())), + ) }; let target_type = if comprehension.is_async() { @@ -4820,15 +4859,14 @@ impl<'db> TypeInferenceBuilder<'db> { if unpack_position == UnpackPosition::First { self.context.extend(unpacked.diagnostics()); } - let target_ast_id = target.scoped_expression_id(self.db(), self.scope()); + let target_ast_id = + target.scoped_expression_id(self.db(), unpack.target_scope(self.db())); unpacked.expression_type(target_ast_id) } - TargetKind::NameOrAttribute => { - iterable_type.try_iterate(self.db()).unwrap_or_else(|err| { - err.report_diagnostic(&self.context, iterable_type, iterable.into()); - err.fallback_element_type(self.db()) - }) - } + TargetKind::Single => iterable_type.try_iterate(self.db()).unwrap_or_else(|err| { + err.report_diagnostic(&self.context, iterable_type, iterable.into()); + err.fallback_element_type(self.db()) + }), } }; @@ -5663,59 +5701,135 @@ impl<'db> TypeInferenceBuilder<'db> { todo_type!("generic `typing.Awaitable` type") } - /// Infer the type of a [`ast::ExprName`] expression, assuming a load context. + // Perform narrowing with applicable constraints between the current scope and the enclosing scope. + fn narrow_with_applicable_constraints( + &self, + expr: &PlaceExpr, + mut ty: Type<'db>, + constraint_keys: &[(FileScopeId, ConstraintKey)], + ) -> Type<'db> { + let db = self.db(); + for (enclosing_scope_file_id, constraint_key) in constraint_keys { + let use_def = self.index.use_def_map(*enclosing_scope_file_id); + let constraints = use_def.narrowing_constraints_at_use(*constraint_key); + let place_table = self.index.place_table(*enclosing_scope_file_id); + let place = place_table.place_id_by_expr(expr).unwrap(); + + ty = constraints.narrow(db, ty, place); + } + ty + } + fn infer_name_load(&mut self, name_node: &ast::ExprName) -> Type<'db> { let ast::ExprName { range: _, id: symbol_name, ctx: _, } = name_node; + let Ok(expr) = PlaceExpr::try_from(symbol_name); + let db = self.db(); + + let (resolved, constraint_keys) = + self.infer_place_load(&expr, ast::ExprRef::Name(name_node)); + resolved + // Not found in the module's explicitly declared global symbols? + // Check the "implicit globals" such as `__doc__`, `__file__`, `__name__`, etc. + // These are looked up as attributes on `types.ModuleType`. + .or_fall_back_to(db, || { + module_type_implicit_global_symbol(db, symbol_name).map_type(|ty| { + self.narrow_with_applicable_constraints(&expr, ty, &constraint_keys) + }) + }) + // Not found in globals? Fallback to builtins + // (without infinite recursion if we're already in builtins.) + .or_fall_back_to(db, || { + if Some(self.scope()) == builtins_module_scope(db) { + Place::Unbound.into() + } else { + builtins_symbol(db, symbol_name) + } + }) + // Still not found? It might be `reveal_type`... + .or_fall_back_to(db, || { + if symbol_name == "reveal_type" { + if let Some(builder) = self.context.report_lint(&UNDEFINED_REVEAL, name_node) { + let mut diag = + builder.into_diagnostic("`reveal_type` used without importing it"); + diag.info( + "This is allowed for debugging convenience but will fail at runtime", + ); + } + typing_extensions_symbol(db, symbol_name) + } else { + Place::Unbound.into() + } + }) + .unwrap_with_diagnostic(|lookup_error| match lookup_error { + LookupError::Unbound(qualifiers) => { + self.report_unresolved_reference(name_node); + TypeAndQualifiers::new(Type::unknown(), qualifiers) + } + LookupError::PossiblyUnbound(type_when_bound) => { + if self.is_reachable(name_node) { + report_possibly_unresolved_reference(&self.context, name_node); + } + type_when_bound + } + }) + .inner_type() + } + fn infer_local_place_load( + &self, + expr: &PlaceExpr, + expr_ref: ast::ExprRef, + ) -> (Place<'db>, Option) { let db = self.db(); let scope = self.scope(); let file_scope_id = scope.file_scope_id(db); - let symbol_table = self.index.symbol_table(file_scope_id); + let place_table = self.index.place_table(file_scope_id); let use_def = self.index.use_def_map(file_scope_id); - let mut constraint_keys = vec![]; - // Perform narrowing with applicable constraints between the current scope and the enclosing scope. - let narrow_with_applicable_constraints = |mut ty, constraint_keys: &[_]| { - for (enclosing_scope_file_id, constraint_key) in constraint_keys { - let use_def = self.index.use_def_map(*enclosing_scope_file_id); - let constraints = use_def.narrowing_constraints_at_use(*constraint_key); - let symbol_table = self.index.symbol_table(*enclosing_scope_file_id); - let symbol = symbol_table.symbol_id_by_name(symbol_name).unwrap(); - - ty = constraints.narrow(db, ty, symbol); - } - ty - }; - // If we're inferring types of deferred expressions, always treat them as public symbols - let (local_scope_symbol, use_id) = if self.is_deferred() { - let symbol = if let Some(symbol_id) = symbol_table.symbol_id_by_name(symbol_name) { - symbol_from_bindings(db, use_def.public_bindings(symbol_id)) + if self.is_deferred() { + let place = if let Some(place_id) = place_table.place_id_by_expr(expr) { + place_from_bindings(db, use_def.public_bindings(place_id)) } else { assert!( self.deferred_state.in_string_annotation(), - "Expected the symbol table to create a symbol for every Name node" + "Expected the place table to create a place for every valid PlaceExpr node" ); - Symbol::Unbound + Place::Unbound }; - (symbol, None) + (place, None) } else { - let use_id = name_node.scoped_use_id(db, scope); - let symbol = symbol_from_bindings(db, use_def.bindings_at_use(use_id)); - (symbol, Some(use_id)) - }; + let use_id = expr_ref.scoped_use_id(db, scope); + let place = place_from_bindings(db, use_def.bindings_at_use(use_id)); + (place, Some(use_id)) + } + } + + /// Infer the type of a place expression, assuming a load context. + fn infer_place_load( + &self, + expr: &PlaceExpr, + expr_ref: ast::ExprRef, + ) -> (PlaceAndQualifiers<'db>, Vec<(FileScopeId, ConstraintKey)>) { + let db = self.db(); + let scope = self.scope(); + let file_scope_id = scope.file_scope_id(db); + let place_table = self.index.place_table(file_scope_id); + + let mut constraint_keys = vec![]; + let (local_scope_place, use_id) = self.infer_local_place_load(expr, expr_ref); - let symbol = SymbolAndQualifiers::from(local_scope_symbol).or_fall_back_to(db, || { - let has_bindings_in_this_scope = match symbol_table.symbol_by_name(symbol_name) { - Some(symbol) => symbol.is_bound(), + let place = PlaceAndQualifiers::from(local_scope_place).or_fall_back_to(db, || { + let has_bindings_in_this_scope = match place_table.place_by_expr(expr) { + Some(place_expr) => place_expr.is_bound(), None => { assert!( self.deferred_state.in_string_annotation(), - "Expected the symbol table to create a symbol for every Name node" + "Expected the place table to create a place for every Name node" ); false } @@ -5723,25 +5837,46 @@ impl<'db> TypeInferenceBuilder<'db> { let current_file = self.file(); - let skip_non_global_scopes = symbol_table - .symbol_id_by_name(symbol_name) - .is_some_and(|symbol_id| self.skip_non_global_scopes(file_scope_id, symbol_id)); + if let Some(name) = expr.as_name() { + let skip_non_global_scopes = place_table + .place_id_by_name(name) + .is_some_and(|symbol_id| self.skip_non_global_scopes(file_scope_id, symbol_id)); - if skip_non_global_scopes { - return global_symbol(self.db(), self.file(), symbol_name); + if skip_non_global_scopes { + return global_symbol(self.db(), self.file(), name); + } } // If it's a function-like scope and there is one or more binding in this scope (but // none of those bindings are visible from where we are in the control flow), we cannot // fallback to any bindings in enclosing scopes. As such, we can immediately short-circuit - // here and return `Symbol::Unbound`. + // here and return `Place::Unbound`. // // This is because Python is very strict in its categorisation of whether a variable is // a local variable or not in function-like scopes. If a variable has any bindings in a // function-like scope, it is considered a local variable; it never references another // scope. (At runtime, it would use the `LOAD_FAST` opcode.) if has_bindings_in_this_scope && scope.is_function_like(db) { - return Symbol::Unbound.into(); + return Place::Unbound.into(); + } + + for root_expr in place_table.root_place_exprs(expr) { + let mut expr_ref = expr_ref; + for _ in 0..(expr.sub_segments().len() - root_expr.sub_segments().len()) { + match expr_ref { + ast::ExprRef::Attribute(attribute) => { + expr_ref = ast::ExprRef::from(&attribute.value); + } + ast::ExprRef::Subscript(subscript) => { + expr_ref = ast::ExprRef::from(&subscript.value); + } + _ => unreachable!(), + } + } + let (parent_place, _use_id) = self.infer_local_place_load(root_expr, expr_ref); + if let Place::Type(_, _) = parent_place { + return Place::Unbound.into(); + } } if let Some(use_id) = use_id { @@ -5751,7 +5886,7 @@ impl<'db> TypeInferenceBuilder<'db> { // Walk up parent scopes looking for a possible enclosing scope that may have a // definition of this name visible to us (would be `LOAD_DEREF` at runtime.) // Note that we skip the scope containing the use that we are resolving, since we - // already looked for the symbol there up above. + // already looked for the place there up above. for (enclosing_scope_file_id, _) in self.index.ancestor_scopes(file_scope_id).skip(1) { // Class scopes are not visible to nested scopes, and we need to handle global // scope differently (because an unbound name there falls back to builtins), so @@ -5766,18 +5901,17 @@ impl<'db> TypeInferenceBuilder<'db> { .parent() .is_some_and(|parent| parent == enclosing_scope_file_id); - // If the reference is in a nested eager scope, we need to look for the symbol at + // If the reference is in a nested eager scope, we need to look for the place at // the point where the previous enclosing scope was defined, instead of at the end // of the scope. (Note that the semantic index builder takes care of only // registering eager bindings for nested scopes that are actually eager, and for // enclosing scopes that actually contain bindings that we should use when // resolving the reference.) if !self.is_deferred() { - match self.index.eager_snapshot( - enclosing_scope_file_id, - symbol_name, - file_scope_id, - ) { + match self + .index + .eager_snapshot(enclosing_scope_file_id, expr, file_scope_id) + { EagerSnapshotResult::FoundConstraint(constraint) => { constraint_keys.push(( enclosing_scope_file_id, @@ -5785,20 +5919,37 @@ impl<'db> TypeInferenceBuilder<'db> { )); } EagerSnapshotResult::FoundBindings(bindings) => { - if !enclosing_scope_id.is_function_like(db) + if expr.is_name() + && !enclosing_scope_id.is_function_like(db) && !is_immediately_enclosing_scope { continue; } - return symbol_from_bindings(db, bindings) + return place_from_bindings(db, bindings) .map_type(|ty| { - narrow_with_applicable_constraints(ty, &constraint_keys) + self.narrow_with_applicable_constraints( + expr, + ty, + &constraint_keys, + ) }) .into(); } // There are no visible bindings / constraint here. - // Don't fall back to non-eager symbol resolution. + // Don't fall back to non-eager place resolution. EagerSnapshotResult::NotFound => { + let enclosing_place_table = + self.index.place_table(enclosing_scope_file_id); + for enclosing_root_place in enclosing_place_table.root_place_exprs(expr) + { + if enclosing_root_place.is_bound() { + if let Place::Type(_, _) = + place(db, enclosing_scope_id, enclosing_root_place).place + { + return Place::Unbound.into(); + } + } + } continue; } EagerSnapshotResult::NoLongerInEagerContext => {} @@ -5809,36 +5960,35 @@ impl<'db> TypeInferenceBuilder<'db> { continue; } - let enclosing_symbol_table = self.index.symbol_table(enclosing_scope_file_id); - let Some(enclosing_symbol) = enclosing_symbol_table.symbol_by_name(symbol_name) - else { + let enclosing_place_table = self.index.place_table(enclosing_scope_file_id); + let Some(enclosing_place) = enclosing_place_table.place_by_expr(expr) else { continue; }; - if enclosing_symbol.is_bound() { + if enclosing_place.is_bound() { // We can return early here, because the nearest function-like scope that // defines a name must be the only source for the nonlocal reference (at // runtime, it is the scope that creates the cell for our closure.) If the name // isn't bound in that scope, we should get an unbound name, not continue // falling back to other scopes / globals / builtins. - return symbol(db, enclosing_scope_id, symbol_name) - .map_type(|ty| narrow_with_applicable_constraints(ty, &constraint_keys)); + return place(db, enclosing_scope_id, expr).map_type(|ty| { + self.narrow_with_applicable_constraints(expr, ty, &constraint_keys) + }); } } - SymbolAndQualifiers::from(Symbol::Unbound) + PlaceAndQualifiers::from(Place::Unbound) // No nonlocal binding? Check the module's explicit globals. // Avoid infinite recursion if `self.scope` already is the module's global scope. .or_fall_back_to(db, || { if file_scope_id.is_global() { - return Symbol::Unbound.into(); + return Place::Unbound.into(); } if !self.is_deferred() { - match self.index.eager_snapshot( - FileScopeId::global(), - symbol_name, - file_scope_id, - ) { + match self + .index + .eager_snapshot(FileScopeId::global(), expr, file_scope_id) + { EagerSnapshotResult::FoundConstraint(constraint) => { constraint_keys.push(( FileScopeId::global(), @@ -5846,72 +5996,35 @@ impl<'db> TypeInferenceBuilder<'db> { )); } EagerSnapshotResult::FoundBindings(bindings) => { - return symbol_from_bindings(db, bindings) + return place_from_bindings(db, bindings) .map_type(|ty| { - narrow_with_applicable_constraints(ty, &constraint_keys) + self.narrow_with_applicable_constraints( + expr, + ty, + &constraint_keys, + ) }) .into(); } // There are no visible bindings / constraint here. EagerSnapshotResult::NotFound => { - return Symbol::Unbound.into(); + return Place::Unbound.into(); } EagerSnapshotResult::NoLongerInEagerContext => {} } } - explicit_global_symbol(db, self.file(), symbol_name) - .map_type(|ty| narrow_with_applicable_constraints(ty, &constraint_keys)) - }) - // Not found in the module's explicitly declared global symbols? - // Check the "implicit globals" such as `__doc__`, `__file__`, `__name__`, etc. - // These are looked up as attributes on `types.ModuleType`. - .or_fall_back_to(db, || { - module_type_implicit_global_symbol(db, symbol_name) - .map_type(|ty| narrow_with_applicable_constraints(ty, &constraint_keys)) - }) - // Not found in globals? Fallback to builtins - // (without infinite recursion if we're already in builtins.) - .or_fall_back_to(db, || { - if Some(self.scope()) == builtins_module_scope(db) { - Symbol::Unbound.into() - } else { - builtins_symbol(db, symbol_name) - } - }) - // Still not found? It might be `reveal_type`... - .or_fall_back_to(db, || { - if symbol_name == "reveal_type" { - if let Some(builder) = - self.context.report_lint(&UNDEFINED_REVEAL, name_node) - { - let mut diag = - builder.into_diagnostic("`reveal_type` used without importing it"); - diag.info( - "This is allowed for debugging convenience but will fail at runtime" - ); - } - typing_extensions_symbol(db, symbol_name) - } else { - Symbol::Unbound.into() - } + let Some(name) = expr.as_name() else { + return Place::Unbound.into(); + }; + + explicit_global_symbol(db, self.file(), name).map_type(|ty| { + self.narrow_with_applicable_constraints(expr, ty, &constraint_keys) + }) }) }); - symbol - .unwrap_with_diagnostic(|lookup_error| match lookup_error { - LookupError::Unbound(qualifiers) => { - self.report_unresolved_reference(name_node); - TypeAndQualifiers::new(Type::unknown(), qualifiers) - } - LookupError::PossiblyUnbound(type_when_bound) => { - if self.is_reachable(name_node) { - report_possibly_unresolved_reference(&self.context, name_node); - } - type_when_bound - } - }) - .inner_type() + (place, constraint_keys) } pub(super) fn report_unresolved_reference(&self, expr_name_node: &ast::ExprName) { @@ -5946,7 +6059,7 @@ impl<'db> TypeInferenceBuilder<'db> { .and_then(|class| { Type::instance(self.db(), class.default_specialization(self.db())) .member(self.db(), id) - .symbol + .place .ignore_possibly_unbound() }) .is_some(); @@ -5975,14 +6088,27 @@ impl<'db> TypeInferenceBuilder<'db> { ctx: _, } = attribute; - let value_type = if self.index.is_standalone_expression(&**value) { - self.infer_standalone_expression(value) - } else { - self.infer_expression(value) - }; - + let value_type = self.infer_maybe_standalone_expression(value); let db = self.db(); + // If `attribute` is a valid reference, we attempt type narrowing by assignment. + if let Ok(place_expr) = PlaceExpr::try_from(attribute) { + let member = value_type.class_member(db, attr.id.clone()); + // If the member is a data descriptor, the value most recently assigned + // to the attribute may not necessarily be obtained here. + if member + .place + .ignore_possibly_unbound() + .is_none_or(|ty| !ty.may_be_data_descriptor(db)) + { + let (resolved, _) = + self.infer_place_load(&place_expr, ast::ExprRef::Attribute(attribute)); + if let Place::Type(ty, Boundness::Bound) = resolved.place { + return ty; + } + } + } + value_type .member(db, &attr.id) .unwrap_with_diagnostic(|lookup_error| match lookup_error { @@ -5992,12 +6118,12 @@ impl<'db> TypeInferenceBuilder<'db> { if report_unresolved_attribute { let bound_on_instance = match value_type { Type::ClassLiteral(class) => { - !class.instance_member(db, None, attr).symbol.is_unbound() + !class.instance_member(db, None, attr).place.is_unbound() } Type::SubclassOf(subclass_of @ SubclassOfType { .. }) => { match subclass_of.subclass_of() { SubclassOfInner::Class(class) => { - !class.instance_member(db, attr).symbol.is_unbound() + !class.instance_member(db, attr).place.is_unbound() } SubclassOfInner::Dynamic(_) => unreachable!( "Attribute lookup on a dynamic `SubclassOf` type should always return a bound symbol" @@ -6504,11 +6630,11 @@ impl<'db> TypeInferenceBuilder<'db> { let right_class = right_ty.to_meta_type(self.db()); if left_ty != right_ty && right_ty.is_subtype_of(self.db(), left_ty) { let reflected_dunder = op.reflected_dunder(); - let rhs_reflected = right_class.member(self.db(), reflected_dunder).symbol; + let rhs_reflected = right_class.member(self.db(), reflected_dunder).place; // TODO: if `rhs_reflected` is possibly unbound, we should union the two possible // Bindings together if !rhs_reflected.is_unbound() - && rhs_reflected != left_class.member(self.db(), reflected_dunder).symbol + && rhs_reflected != left_class.member(self.db(), reflected_dunder).place { return right_ty .try_call_dunder( @@ -7318,9 +7444,9 @@ impl<'db> TypeInferenceBuilder<'db> { ) -> Result, CompareUnsupportedError<'db>> { let db = self.db(); - let contains_dunder = right.class_member(db, "__contains__".into()).symbol; + let contains_dunder = right.class_member(db, "__contains__".into()).place; let compare_result_opt = match contains_dunder { - Symbol::Type(contains_dunder, Boundness::Bound) => { + Place::Type(contains_dunder, Boundness::Bound) => { // If `__contains__` is available, it is used directly for the membership test. contains_dunder .try_call(db, &CallArgumentTypes::positional([right, left])) @@ -7440,12 +7566,80 @@ impl<'db> TypeInferenceBuilder<'db> { } fn infer_subscript_expression(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { + let ast::ExprSubscript { + value, + slice, + range: _, + ctx, + } = subscript; + + match ctx { + ExprContext::Load => self.infer_subscript_load(subscript), + ExprContext::Store | ExprContext::Del => { + let value_ty = self.infer_expression(value); + let slice_ty = self.infer_expression(slice); + self.infer_subscript_expression_types(value, value_ty, slice_ty); + Type::Never + } + ExprContext::Invalid => { + let value_ty = self.infer_expression(value); + let slice_ty = self.infer_expression(slice); + self.infer_subscript_expression_types(value, value_ty, slice_ty); + Type::unknown() + } + } + } + + fn infer_subscript_load(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { let ast::ExprSubscript { range: _, value, slice, ctx: _, } = subscript; + let db = self.db(); + let value_ty = self.infer_expression(value); + + // If `value` is a valid reference, we attempt type narrowing by assignment. + if !value_ty.is_unknown() { + if let Ok(expr) = PlaceExpr::try_from(subscript) { + // Type narrowing based on assignment to a subscript expression is generally + // unsound, because arbitrary `__getitem__`/`__setitem__` methods on a class do not + // necessarily guarantee that the passed-in value for `__setitem__` is stored and + // can be retrieved unmodified via `__getitem__`. Therefore, we currently only + // perform assignment-based narrowing on a few built-in classes (`list`, `dict`, + // `bytesarray`, `TypedDict` and `collections` types) where we are confident that + // this kind of narrowing can be performed soundly. This is the same approach as + // pyright. TODO: Other standard library classes may also be considered safe. Also, + // subclasses of these safe classes that do not override `__getitem__/__setitem__` + // may be considered safe. + let safe_mutable_classes = [ + KnownClass::List.to_instance(db), + KnownClass::Dict.to_instance(db), + KnownClass::Bytearray.to_instance(db), + KnownClass::DefaultDict.to_instance(db), + SpecialFormType::ChainMap.instance_fallback(db), + SpecialFormType::Counter.instance_fallback(db), + SpecialFormType::Deque.instance_fallback(db), + SpecialFormType::OrderedDict.instance_fallback(db), + SpecialFormType::TypedDict.instance_fallback(db), + ]; + if safe_mutable_classes.iter().any(|safe_mutable_class| { + value_ty.is_equivalent_to(db, *safe_mutable_class) + || value_ty + .generic_origin(db) + .zip(safe_mutable_class.generic_origin(db)) + .is_some_and(|(l, r)| l == r) + }) { + let (place, _) = + self.infer_place_load(&expr, ast::ExprRef::Subscript(subscript)); + if let Place::Type(ty, Boundness::Bound) = place.place { + self.infer_expression(slice); + return ty; + } + } + } + } // HACK ALERT: If we are subscripting a generic class, short-circuit the rest of the // subscript inference logic and treat this as an explicit specialization. @@ -7453,7 +7647,6 @@ impl<'db> TypeInferenceBuilder<'db> { // this callable as the `__class_getitem__` method on `type`. That probably requires // updating all of the subscript logic below to use custom callables for all of the _other_ // special cases, too. - let value_ty = self.infer_expression(value); if let Type::ClassLiteral(class) = value_ty { if class.is_known(self.db(), KnownClass::Tuple) { self.infer_expression(slice); @@ -7747,11 +7940,11 @@ impl<'db> TypeInferenceBuilder<'db> { // method in these `sys.version_info` branches. if value_ty.is_subtype_of(self.db(), KnownClass::Type.to_instance(self.db())) { let dunder_class_getitem_method = - value_ty.member(self.db(), "__class_getitem__").symbol; + value_ty.member(self.db(), "__class_getitem__").place; match dunder_class_getitem_method { - Symbol::Unbound => {} - Symbol::Type(ty, boundness) => { + Place::Unbound => {} + Place::Type(ty, boundness) => { if boundness == Boundness::PossiblyUnbound { if let Some(builder) = self .context @@ -9209,7 +9402,7 @@ impl<'db> TypeInferenceBuilder<'db> { // TODO: Check that value type is enum otherwise return None value_ty .member(self.db(), &attr.id) - .symbol + .place .ignore_possibly_unbound() .unwrap_or(Type::unknown()) } @@ -9510,10 +9703,10 @@ fn contains_string_literal(expr: &ast::Expr) -> bool { #[cfg(test)] mod tests { use crate::db::tests::{TestDb, setup_db}; + use crate::place::{global_symbol, symbol}; use crate::semantic_index::definition::Definition; - use crate::semantic_index::symbol::FileScopeId; - use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map}; - use crate::symbol::global_symbol; + use crate::semantic_index::place::FileScopeId; + use crate::semantic_index::{global_scope, place_table, semantic_index, use_def_map}; use crate::types::check_types; use ruff_db::diagnostic::Diagnostic; use ruff_db::files::{File, system_path_to_file}; @@ -9528,7 +9721,7 @@ mod tests { file_name: &str, scopes: &[&str], symbol_name: &str, - ) -> Symbol<'db> { + ) -> Place<'db> { let file = system_path_to_file(db, file_name).expect("file to exist"); let index = semantic_index(db, file); let mut file_scope_id = FileScopeId::global(); @@ -9543,7 +9736,7 @@ mod tests { assert_eq!(scope.name(db), *expected_scope_name); } - symbol(db, scope, symbol_name).symbol + symbol(db, scope, symbol_name).place } #[track_caller] @@ -9698,7 +9891,7 @@ mod tests { assert_eq!(var_ty.display(&db).to_string(), "typing.TypeVar"); let expected_name_ty = format!(r#"Literal["{var}"]"#); - let name_ty = var_ty.member(&db, "__name__").symbol.expect_type(); + 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 { @@ -9788,8 +9981,8 @@ mod tests { 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) - .public_bindings(symbol_table(db, scope).symbol_id_by_name(name).unwrap()) - .find_map(|b| b.binding) + .public_bindings(place_table(db, scope).place_id_by_name(name).unwrap()) + .find_map(|b| b.binding.definition()) .expect("no binding found") } @@ -9803,7 +9996,7 @@ mod tests { ])?; let a = system_path_to_file(&db, "/src/a.py").unwrap(); - let x_ty = global_symbol(&db, a, "x").symbol.expect_type(); + let x_ty = global_symbol(&db, a, "x").place.expect_type(); assert_eq!(x_ty.display(&db).to_string(), "int"); @@ -9812,7 +10005,7 @@ mod tests { let a = system_path_to_file(&db, "/src/a.py").unwrap(); - let x_ty_2 = global_symbol(&db, a, "x").symbol.expect_type(); + let x_ty_2 = global_symbol(&db, a, "x").place.expect_type(); assert_eq!(x_ty_2.display(&db).to_string(), "bool"); @@ -9829,7 +10022,7 @@ mod tests { ])?; let a = system_path_to_file(&db, "/src/a.py").unwrap(); - let x_ty = global_symbol(&db, a, "x").symbol.expect_type(); + let x_ty = global_symbol(&db, a, "x").place.expect_type(); assert_eq!(x_ty.display(&db).to_string(), "int"); @@ -9839,7 +10032,7 @@ mod tests { db.clear_salsa_events(); - let x_ty_2 = global_symbol(&db, a, "x").symbol.expect_type(); + let x_ty_2 = global_symbol(&db, a, "x").place.expect_type(); assert_eq!(x_ty_2.display(&db).to_string(), "int"); @@ -9865,7 +10058,7 @@ mod tests { ])?; let a = system_path_to_file(&db, "/src/a.py").unwrap(); - let x_ty = global_symbol(&db, a, "x").symbol.expect_type(); + let x_ty = global_symbol(&db, a, "x").place.expect_type(); assert_eq!(x_ty.display(&db).to_string(), "int"); @@ -9875,7 +10068,7 @@ mod tests { db.clear_salsa_events(); - let x_ty_2 = global_symbol(&db, a, "x").symbol.expect_type(); + let x_ty_2 = global_symbol(&db, a, "x").place.expect_type(); assert_eq!(x_ty_2.display(&db).to_string(), "int"); @@ -9922,7 +10115,7 @@ mod tests { )?; let file_main = system_path_to_file(&db, "/src/main.py").unwrap(); - let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type(); + 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 @@ -9937,7 +10130,7 @@ mod tests { let events = { db.clear_salsa_events(); - let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type(); + 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() }; @@ -9956,7 +10149,7 @@ mod tests { let events = { db.clear_salsa_events(); - let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type(); + 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() }; @@ -10007,7 +10200,7 @@ mod tests { )?; let file_main = system_path_to_file(&db, "/src/main.py").unwrap(); - let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type(); + 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 @@ -10024,7 +10217,7 @@ mod tests { let events = { db.clear_salsa_events(); - let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type(); + 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() }; @@ -10045,7 +10238,7 @@ mod tests { let events = { db.clear_salsa_events(); - let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type(); + 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() }; diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index e727655990ad8..c62c6f0b4f6b8 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -4,7 +4,7 @@ use std::marker::PhantomData; use super::protocol_class::ProtocolInterface; use super::{ClassType, KnownClass, SubclassOfType, Type}; -use crate::symbol::{Boundness, Symbol, SymbolAndQualifiers}; +use crate::place::{Boundness, Place, PlaceAndQualifiers}; use crate::types::{ClassLiteral, TypeMapping, TypeVarInstance}; use crate::{Db, FxOrderSet}; @@ -47,8 +47,8 @@ impl<'db> Type<'db> { // TODO: this should consider the types of the protocol members protocol.inner.interface(db).members(db).all(|member| { matches!( - self.member(db, member.name()).symbol, - Symbol::Type(_, Boundness::Bound) + self.member(db, member.name()).place, + Place::Type(_, Boundness::Bound) ) }) } @@ -294,14 +294,14 @@ impl<'db> ProtocolInstanceType<'db> { false } - pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { + pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { match self.inner { Protocol::FromClass(class) => class.instance_member(db, name), Protocol::Synthesized(synthesized) => synthesized .interface() .member_by_name(db, name) - .map(|member| SymbolAndQualifiers { - symbol: Symbol::bound(member.ty()), + .map(|member| PlaceAndQualifiers { + place: Place::bound(member.ty()), qualifiers: member.qualifiers(), }) .unwrap_or_else(|| KnownClass::Object.to_instance(db).instance_member(db, name)), diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index f8bf6d610bc96..e6cc62c383758 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -1,11 +1,11 @@ use crate::Db; use crate::semantic_index::ast_ids::HasScopedExpressionId; use crate::semantic_index::expression::Expression; +use crate::semantic_index::place::{PlaceTable, ScopeId, ScopedPlaceId}; +use crate::semantic_index::place_table; use crate::semantic_index::predicate::{ PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, }; -use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId, SymbolTable}; -use crate::semantic_index::symbol_table; use crate::types::function::KnownFunction; use crate::types::infer::infer_same_file_expression_type; use crate::types::{ @@ -42,7 +42,7 @@ use super::UnionType; pub(crate) fn infer_narrowing_constraint<'db>( db: &'db dyn Db, predicate: Predicate<'db>, - symbol: ScopedSymbolId, + place: ScopedPlaceId, ) -> Option> { let constraints = match predicate.node { PredicateNode::Expression(expression) => { @@ -62,7 +62,7 @@ pub(crate) fn infer_narrowing_constraint<'db>( PredicateNode::StarImportPlaceholder(_) => return None, }; if let Some(constraints) = constraints { - constraints.get(&symbol).copied() + constraints.get(&place).copied() } else { None } @@ -190,7 +190,7 @@ impl ClassInfoConstraintFunction { } } -type NarrowingConstraints<'db> = FxHashMap>; +type NarrowingConstraints<'db> = FxHashMap>; fn merge_constraints_and<'db>( into: &mut NarrowingConstraints<'db>, @@ -235,7 +235,7 @@ fn merge_constraints_or<'db>( } fn negate_if<'db>(constraints: &mut NarrowingConstraints<'db>, db: &'db dyn Db, yes: bool) { - for (_symbol, ty) in constraints.iter_mut() { + for (_place, ty) in constraints.iter_mut() { *ty = ty.negate_if(db, yes); } } @@ -347,8 +347,8 @@ impl<'db> NarrowingConstraintsBuilder<'db> { }) } - fn symbols(&self) -> &'db SymbolTable { - symbol_table(self.db, self.scope()) + fn places(&self) -> &'db PlaceTable { + place_table(self.db, self.scope()) } fn scope(&self) -> ScopeId<'db> { @@ -360,9 +360,9 @@ impl<'db> NarrowingConstraintsBuilder<'db> { } #[track_caller] - fn expect_expr_name_symbol(&self, symbol: &str) -> ScopedSymbolId { - self.symbols() - .symbol_id_by_name(symbol) + fn expect_expr_name_symbol(&self, symbol: &str) -> ScopedPlaceId { + self.places() + .place_id_by_name(symbol) .expect("We should always have a symbol for every `Name` node") } diff --git a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs index c419b1c31b662..10dfac9c07288 100644 --- a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs +++ b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs @@ -1,5 +1,5 @@ use crate::db::tests::TestDb; -use crate::symbol::{builtins_symbol, known_module_symbol}; +use crate::place::{builtins_symbol, known_module_symbol}; use crate::types::{ BoundMethodType, CallableType, IntersectionBuilder, KnownClass, Parameter, Parameters, Signature, SpecialFormType, SubclassOfType, TupleType, Type, UnionType, @@ -130,20 +130,20 @@ impl Ty { Ty::LiteralString => Type::LiteralString, Ty::BytesLiteral(s) => Type::bytes_literal(db, s.as_bytes()), Ty::BuiltinInstance(s) => builtins_symbol(db, s) - .symbol + .place .expect_type() .to_instance(db) .unwrap(), Ty::AbcInstance(s) => known_module_symbol(db, KnownModule::Abc, s) - .symbol + .place .expect_type() .to_instance(db) .unwrap(), Ty::AbcClassLiteral(s) => known_module_symbol(db, KnownModule::Abc, s) - .symbol + .place .expect_type(), Ty::TypingLiteral => Type::SpecialForm(SpecialFormType::Literal), - Ty::BuiltinClassLiteral(s) => builtins_symbol(db, s).symbol.expect_type(), + Ty::BuiltinClassLiteral(s) => builtins_symbol(db, s).place.expect_type(), Ty::KnownClassInstance(known_class) => known_class.to_instance(db), Ty::Union(tys) => { UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db))) @@ -166,7 +166,7 @@ impl Ty { Ty::SubclassOfBuiltinClass(s) => SubclassOfType::from( db, builtins_symbol(db, s) - .symbol + .place .expect_type() .expect_class_literal() .default_specialization(db), @@ -174,17 +174,17 @@ impl Ty { Ty::SubclassOfAbcClass(s) => SubclassOfType::from( db, known_module_symbol(db, KnownModule::Abc, s) - .symbol + .place .expect_type() .expect_class_literal() .default_specialization(db), ), Ty::AlwaysTruthy => Type::AlwaysTruthy, Ty::AlwaysFalsy => Type::AlwaysFalsy, - Ty::BuiltinsFunction(name) => builtins_symbol(db, name).symbol.expect_type(), + Ty::BuiltinsFunction(name) => builtins_symbol(db, name).place.expect_type(), Ty::BuiltinsBoundMethod { class, method } => { - let builtins_class = builtins_symbol(db, class).symbol.expect_type(); - let function = builtins_class.member(db, method).symbol.expect_type(); + let builtins_class = builtins_symbol(db, class).place.expect_type(); + let function = builtins_class.member(db, method).place.expect_type(); create_bound_method(db, function, builtins_class) } diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index dbf7837a0c60d..b17864490e32e 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -5,10 +5,11 @@ use itertools::{Either, Itertools}; use ruff_python_ast::name::Name; use crate::{ - semantic_index::{symbol_table, use_def_map}, - symbol::{symbol_from_bindings, symbol_from_declarations}, - types::function::KnownFunction, - types::{ClassBase, ClassLiteral, Type, TypeMapping, TypeQualifiers, TypeVarInstance}, + place::{place_from_bindings, place_from_declarations}, + semantic_index::{place_table, use_def_map}, + types::{ + ClassBase, ClassLiteral, KnownFunction, Type, TypeMapping, TypeQualifiers, TypeVarInstance, + }, {Db, FxOrderSet}, }; @@ -321,19 +322,19 @@ fn cached_protocol_interface<'db>( { let parent_scope = parent_protocol.body_scope(db); let use_def_map = use_def_map(db, parent_scope); - let symbol_table = symbol_table(db, parent_scope); + let place_table = place_table(db, parent_scope); members.extend( use_def_map .all_public_declarations() - .flat_map(|(symbol_id, declarations)| { - symbol_from_declarations(db, declarations).map(|symbol| (symbol_id, symbol)) + .flat_map(|(place_id, declarations)| { + place_from_declarations(db, declarations).map(|place| (place_id, place)) }) - .filter_map(|(symbol_id, symbol)| { - symbol - .symbol + .filter_map(|(place_id, place)| { + place + .place .ignore_possibly_unbound() - .map(|ty| (symbol_id, ty, symbol.qualifiers)) + .map(|ty| (place_id, ty, place.qualifiers)) }) // Bindings in the class body that are not declared in the class body // are not valid protocol members, and we plan to emit diagnostics for them @@ -346,14 +347,18 @@ fn cached_protocol_interface<'db>( .chain( use_def_map .all_public_bindings() - .filter_map(|(symbol_id, bindings)| { - symbol_from_bindings(db, bindings) + .filter_map(|(place_id, bindings)| { + place_from_bindings(db, bindings) .ignore_possibly_unbound() - .map(|ty| (symbol_id, ty, TypeQualifiers::default())) + .map(|ty| (place_id, ty, TypeQualifiers::default())) }), ) - .map(|(symbol_id, member, qualifiers)| { - (symbol_table.symbol(symbol_id).name(), member, qualifiers) + .filter_map(|(place_id, member, qualifiers)| { + Some(( + place_table.place_expr(place_id).as_name()?, + member, + qualifiers, + )) }) .filter(|(name, _, _)| !excluded_from_proto_members(name)) .map(|(name, ty, qualifiers)| { diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 201ab0eddf836..1f440f782f576 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -1533,16 +1533,15 @@ pub(crate) enum ParameterForm { mod tests { use super::*; use crate::db::tests::{TestDb, setup_db}; - use crate::symbol::global_symbol; - use crate::types::KnownClass; - use crate::types::function::FunctionType; + use crate::place::global_symbol; + use crate::types::{FunctionType, KnownClass}; use ruff_db::system::DbWithWritableSystem as _; #[track_caller] fn get_function_f<'db>(db: &'db TestDb, file: &'static str) -> FunctionType<'db> { let module = ruff_db::files::system_path_to_file(db, file).unwrap(); global_symbol(db, module, "f") - .symbol + .place .expect_type() .expect_function_literal() } diff --git a/crates/ty_python_semantic/src/types/slots.rs b/crates/ty_python_semantic/src/types/slots.rs index 760185db98a08..e5165fb69ba08 100644 --- a/crates/ty_python_semantic/src/types/slots.rs +++ b/crates/ty_python_semantic/src/types/slots.rs @@ -1,7 +1,7 @@ use ruff_python_ast as ast; use crate::db::Db; -use crate::symbol::{Boundness, Symbol}; +use crate::place::{Boundness, Place}; use crate::types::class_base::ClassBase; use crate::types::diagnostic::report_base_with_incompatible_slots; use crate::types::{ClassLiteral, Type}; @@ -24,7 +24,7 @@ enum SlotsKind { impl SlotsKind { fn from(db: &dyn Db, base: ClassLiteral) -> Self { - let Symbol::Type(slots_ty, bound) = base.own_class_member(db, None, "__slots__").symbol + let Place::Type(slots_ty, bound) = base.own_class_member(db, None, "__slots__").place else { return Self::NotSpecified; }; diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index b177ea92f6270..b2febf439be6c 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -1,4 +1,4 @@ -use crate::symbol::SymbolAndQualifiers; +use crate::place::PlaceAndQualifiers; use crate::types::{ ClassType, DynamicType, KnownClass, MemberLookupPolicy, Type, TypeMapping, TypeVarInstance, }; @@ -99,7 +99,7 @@ impl<'db> SubclassOfType<'db> { db: &'db dyn Db, name: &str, policy: MemberLookupPolicy, - ) -> Option> { + ) -> Option> { Type::from(self.subclass_of).find_name_in_mro_with_policy(db, name, policy) } diff --git a/crates/ty_python_semantic/src/types/unpacker.rs b/crates/ty_python_semantic/src/types/unpacker.rs index 07c10ce6835c1..f06ad7c517189 100644 --- a/crates/ty_python_semantic/src/types/unpacker.rs +++ b/crates/ty_python_semantic/src/types/unpacker.rs @@ -7,7 +7,7 @@ use ruff_python_ast::{self as ast, AnyNodeRef}; use crate::Db; use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId}; -use crate::semantic_index::symbol::ScopeId; +use crate::semantic_index::place::ScopeId; use crate::types::{Type, TypeCheckDiagnostics, infer_expression_types}; use crate::unpack::{UnpackKind, UnpackValue}; @@ -84,7 +84,7 @@ impl<'db> Unpacker<'db> { value_ty: Type<'db>, ) { match target { - ast::Expr::Name(_) | ast::Expr::Attribute(_) => { + ast::Expr::Name(_) | ast::Expr::Attribute(_) | ast::Expr::Subscript(_) => { self.targets.insert( target.scoped_expression_id(self.db(), self.target_scope), value_ty, diff --git a/crates/ty_python_semantic/src/unpack.rs b/crates/ty_python_semantic/src/unpack.rs index 338c9a945a3c1..0e34dbe7654c3 100644 --- a/crates/ty_python_semantic/src/unpack.rs +++ b/crates/ty_python_semantic/src/unpack.rs @@ -6,7 +6,7 @@ use crate::Db; use crate::ast_node_ref::AstNodeRef; use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId}; use crate::semantic_index::expression::Expression; -use crate::semantic_index::symbol::{FileScopeId, ScopeId}; +use crate::semantic_index::place::{FileScopeId, ScopeId}; /// This ingredient represents a single unpacking. ///