From 0858896bc434bb7666b7230bde52d2113f328ac9 Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama <45118249+mtshiba@users.noreply.github.com> Date: Thu, 5 Jun 2025 09:24:27 +0900 Subject: [PATCH] [ty] type narrowing by attribute/subscript assignments (#18041) ## Summary This PR partially solves https://github.com/astral-sh/ty/issues/164 (derived from #17643). Currently, the definitions we manage are limited to those for simple name (symbol) targets, but we expand this to track definitions for attribute and subscript targets as well. This was originally planned as part of the work in #17643, but the changes are significant, so I made it a separate PR. After merging this PR, I will reflect this changes in #17643. There is still some incomplete work remaining, but the basic features have been implemented, so I am publishing it as a draft PR. Here is the TODO list (there may be more to come): * [x] Complete rewrite and refactoring of documentation (removing `Symbol` and replacing it with `Place`) * [x] More thorough testing * [x] Consolidation of duplicated code (maybe we can consolidate the handling related to name, attribute, and subscript) This PR replaces the current `Symbol` API with the `Place` API, which is a concept that includes attributes and subscripts (the term is borrowed from Rust). ## Test Plan `mdtest/narrow/assignment.md` is added. --------- Co-authored-by: David Peter Co-authored-by: Carl Meyer --- ...xprs_not_found_in_evaluate_expr_compare.py | 11 + .../resources/mdtest/attributes.md | 83 +- .../resources/mdtest/descriptor_protocol.md | 4 +- .../resources/mdtest/narrow/assignment.md | 318 ++++++ .../mdtest/narrow/conditionals/nested.md | 177 ++++ crates/ty_python_semantic/src/dunder_all.rs | 2 +- crates/ty_python_semantic/src/lib.rs | 2 +- .../src/{symbol.rs => place.rs} | 505 +++++----- .../ty_python_semantic/src/semantic_index.rs | 228 ++--- .../src/semantic_index/ast_ids.rs | 24 +- .../src/semantic_index/builder.rs | 567 ++++------- .../src/semantic_index/definition.rs | 151 ++- .../src/semantic_index/expression.rs | 2 +- .../semantic_index/narrowing_constraints.rs | 6 +- .../src/semantic_index/place.rs | 942 ++++++++++++++++++ .../src/semantic_index/predicate.rs | 6 +- .../src/semantic_index/symbol.rs | 589 ----------- .../src/semantic_index/use_def.rs | 460 ++++----- .../{symbol_state.rs => place_state.rs} | 153 +-- .../semantic_index/visibility_constraints.rs | 18 +- .../ty_python_semantic/src/semantic_model.rs | 2 +- crates/ty_python_semantic/src/types.rs | 421 ++++---- .../ty_python_semantic/src/types/call/bind.rs | 8 +- crates/ty_python_semantic/src/types/class.rs | 186 ++-- .../ty_python_semantic/src/types/context.rs | 2 +- .../ty_python_semantic/src/types/display.rs | 4 +- .../ty_python_semantic/src/types/function.rs | 12 +- .../src/types/ide_support.rs | 48 +- crates/ty_python_semantic/src/types/infer.rs | 793 +++++++++------ .../ty_python_semantic/src/types/instance.rs | 12 +- crates/ty_python_semantic/src/types/narrow.rs | 22 +- .../types/property_tests/type_generation.rs | 20 +- .../src/types/protocol_class.rs | 37 +- .../src/types/signatures.rs | 7 +- crates/ty_python_semantic/src/types/slots.rs | 4 +- .../src/types/subclass_of.rs | 4 +- .../ty_python_semantic/src/types/unpacker.rs | 4 +- crates/ty_python_semantic/src/unpack.rs | 2 +- 38 files changed, 3432 insertions(+), 2404 deletions(-) create mode 100644 crates/ty_project/resources/test/corpus/sub_exprs_not_found_in_evaluate_expr_compare.py create mode 100644 crates/ty_python_semantic/resources/mdtest/narrow/assignment.md rename crates/ty_python_semantic/src/{symbol.rs => place.rs} (72%) create mode 100644 crates/ty_python_semantic/src/semantic_index/place.rs delete mode 100644 crates/ty_python_semantic/src/semantic_index/symbol.rs rename crates/ty_python_semantic/src/semantic_index/use_def/{symbol_state.rs => place_state.rs} (84%) 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 0000000000..7dc644bec9 --- /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 0b74ab50aa..71288e5109 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 cc9661b54f..86b16e4202 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 0000000000..73d676a2a3 --- /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 6b001ea094..b3b077f1bc 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 4265de2ac6..6976e35e22 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 1a204734c0..0123d28c17 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 d2b2518e61..0f25a3ff30 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 [`symbol_from_bindings`]. +/// 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 [`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 c3a2f19418..c117b7f737 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 0a18330569..191be73c23 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 a5b0819cac..db4cf3da50 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 { + fn current_place_table(&mut self) -> &mut PlaceTableBuilder { let scope_id = self.current_scope(); - &mut self.symbol_tables[scope_id] - } - - fn current_attribute_table(&mut self) -> &mut SymbolTableBuilder { - 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::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()); + 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 is_use { - self.mark_symbol_used(symbol); - let use_id = self.current_ast_ids().record_use(expr); + if accessed_object_refers_to_first_parameter && place_expr.is_member() { + place_expr.mark_instance_attribute(); + } + } + + 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::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 => {} + } + } + + 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_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); - } - 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, - }, - ); - } - None => {} - } - } - - if let Some(unpack_position) = self - .current_assignment_mut() - .and_then(CurrentAssignment::unpack_position_mut) - { - *unpack_position = UnpackPosition::Other; + .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 c8720d7cf7..3adbeb68c6 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 f3afd45f49..1c5178b244 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 83bfb0d25d..fa6280ead6 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 0000000000..4862c61b2a --- /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 86e3ba903a..06b71396e7 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 4d25978bed..0000000000 --- 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 7605843db4..39647ac7fa 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>, + bindings_by_declaration: FxHashMap, Bindings>, - /// [`SymbolState`] visible at end of scope for each symbol. - public_symbols: IndexVec, - - /// [`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, + place: ScopedPlaceId, ) -> BindingWithConstraintsIterator<'_, 'db> { - self.bindings_iterator(self.public_symbols[symbol].bindings()) - } - - pub(crate) fn instance_attribute_bindings( - &self, - symbol: ScopedSymbolId, - ) -> 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_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 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 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,16 +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 { + 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, @@ -1107,27 +1085,11 @@ impl<'db> UseDefMapBuilder<'db> { ); } 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 { - 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), + 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 0a7df85a83..d73134ead6 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 4e2e76e301..60db138c57 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 bc4204386e..4a85f138a3 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 b2d1c6b91f..ebccf4a0e7 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 3d349abf17..14906fd6da 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 205e587fad..ffc558e969 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 f0596b0318..498a1b644a 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 645a102e84..02f38c5701 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 2911fbb1d6..3b67e9c682 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 c8f150aa43..13a1ec3487 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 5e485b1346..b0e6a19ddd 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)) + } + } - 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(), + /// 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 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 e727655990..c62c6f0b4f 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 f8bf6d610b..e6cc62c383 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 c419b1c31b..10dfac9c07 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 dbf7837a0c..b17864490e 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 201ab0eddf..1f440f782f 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 760185db98..e5165fb69b 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 b177ea92f6..b2febf439b 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 07c10ce683..f06ad7c517 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 338c9a945a..0e34dbe765 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. ///