Compare commits

..

11 Commits

Author SHA1 Message Date
Dhruv Manilawala
c705787bb3 WIP 2025-02-22 10:59:20 +05:30
Douglas Creager
4dae09ecff [red-knot] Better handling of visibility constraint copies (#16276)
Two related changes.  For context:

1. We were maintaining two separate arenas of `Constraint`s in each
use-def map. One was used for narrowing constraints, and the other for
visibility constraints. The visibility constraint arena was interned,
ensuring that we always used the same ID for any particular
`Constraint`. The narrowing constraint arena was not interned.

2. The TDD code relies on _all_ TDD nodes being interned and reduced.
This is an important requirement for TDDs to be a canonical form, which
allows us to use a single int comparison to test for "always true/false"
and to compare two TDDs for equivalence. But we also need to support an
individual `Constraint` having multiple values in a TDD evaluation (e.g.
to handle a `while` condition having different values the first time
it's evaluated vs later times). Previously, we handled that by
introducing a "copy" number, which was only there as a disambiguator, to
allow an interned, deduplicated constraint ID to appear in the TDD
formula multiple times.

A better way to handle (2) is to not intern the constraints in the
visibility constraint arena! The caller now gets to decide: if they add
a `Constraint` to the arena more than once, they get distinct
`ScopedConstraintId`s — which the TDD code will treat as distinct
variables, allowing them to take on different values in the ternary
function.

With that in place, we can then consolidate on a single (non-interned)
arena, which is shared for both narrowing and visibility constraints.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-02-21 09:16:25 -05:00
Darius Carrier
b9b094869a [pylint] Fix false positives, add missing methods, and support positional-only parameters (PLE0302) (#16263)
## Summary

Resolves 3/4 requests in #16217:

-  Remove not special methods: `__cmp__`, `__div__`, `__nonzero__`, and
`__unicode__`.
-  Add special methods: `__next__`, `__buffer__`, `__class_getitem__`,
`__mro_entries__`, `__release_buffer__`, and `__subclasshook__`.
-  Support positional-only arguments.
-  Add support for module functions `__dir__` and `__getattr__`. As
mentioned in the issue the check is scoped for methods rather than
module functions. I am hesitant to expand the scope of this check
without a discussion.

## Test Plan

- Manually confirmed each example file from the issue functioned as
expected.
- Ran cargo nextest to ensure `unexpected_special_method_signature` test
still passed.

Fixes #16217.
2025-02-21 08:38:51 -05:00
Alex Waygood
b3c5932fda [red-knot] Restrict visibility of the module_type_symbols function (#16290) 2025-02-21 10:55:22 +00:00
Alex Waygood
fe3ae587ea [red-knot] Fix subtle detail in where the types.ModuleType attribute lookup should happen in TypeInferenceBuilder::infer_name_load() (#16284) 2025-02-21 10:48:52 +00:00
Dhruv Manilawala
c2b9fa84f7 Refactor workspace logic into workspace.rs (#16295)
## Summary

This is just a small refactor to move workspace related structs and impl
out from `server.rs` where `Server` is defined and into a new
`workspace.rs`.
2025-02-21 08:37:29 +00:00
Victorien
793264db13 [ruff] Add more Pydantic models variants to the list of default copy semantics (RUF012) (#16291) 2025-02-21 08:28:13 +01:00
Carl Meyer
4d63c16c19 [red-knot] update to latest Salsa (#16293)
Update to latest Salsa main branch. This provides a point of comparison
for the perf impact of fixpoint iteration, which is based on latest
Salsa main.

This requires an update to the locked version of our boxcar dep, since
Salsa now depends on a newer version of boxcar.
2025-02-20 18:15:58 -08:00
David Peter
d2e034adcd [red-knot] Method calls and the descriptor protocol (#16121)
## Summary

This PR achieves the following:

* Add support for checking method calls, and inferring return types from
method calls. For example:
  ```py
  reveal_type("abcde".find("abc"))  # revealed: int
  reveal_type("foo".encode(encoding="utf-8"))  # revealed: bytes
  
  "abcde".find(123)  # error: [invalid-argument-type]
  
  class C:
      def f(self) -> int:
          pass
  
  reveal_type(C.f)  # revealed: <function `f`>
  reveal_type(C().f)  # revealed: <bound method: `f` of `C`>
  
  C.f()  # error: [missing-argument]
  reveal_type(C().f())  # revealed: int
  ```
* Implement the descriptor protocol, i.e. properly call the `__get__`
method when a descriptor object is accessed through a class object or an
instance of a class. For example:
  ```py
  from typing import Literal
  
  class Ten:
def __get__(self, instance: object, owner: type | None = None) ->
Literal[10]:
          return 10
  
  class C:
      ten: Ten = Ten()
  
  reveal_type(C.ten)  # revealed: Literal[10]
  reveal_type(C().ten)  # revealed: Literal[10]
  ```
* Add support for member lookup on intersection types.
* Support type inference for `inspect.getattr_static(obj, attr)` calls.
This was mostly used as a debugging tool during development, but seems
more generally useful. It can be used to bypass the descriptor protocol.
For the example above:
  ```py
  from inspect import getattr_static
  
  reveal_type(getattr_static(C, "ten"))  # revealed: Ten
  ```
* Add a new `Type::Callable(…)` variant with the following sub-variants:
* `Type::Callable(CallableType::BoundMethod(…))` — represents bound
method objects, e.g. `C().f` above
* `Type::Callable(CallableType::MethodWrapperDunderGet(…))` — represents
`f.__get__` where `f` is a function
* `Type::Callable(WrapperDescriptorDunderGet)` — represents
`FunctionType.__get__`
* Add new known classes:
  * `types.MethodType`
  * `types.MethodWrapperType`
  * `types.WrapperDescriptorType`
  * `builtins.range`

## Performance analysis

On this branch, we do more work. We need to do more call checking, since
we now check all method calls. We also need to do ~twice as many member
lookups, because we need to check if a `__get__` attribute exists on
accessed members.

A brief analysis on `tomllib` shows that we now call `Type::call` 1780
times, compared to 612 calls before.

## Limitations

* Data descriptors are not yet supported, i.e. we do not infer correct
types for descriptor attribute accesses in `Store` context and do not
check writes to descriptor attributes. I felt like this was something
that could be split out as a follow-up without risking a major
architectural change.
* We currently distinguish between `Type::member` (with descriptor
protocol) and `Type::static_member` (without descriptor protocol). The
former corresponds to `obj.attr`, the latter corresponds to
`getattr_static(obj, "attr")`. However, to model some details correctly,
we would also need to distinguish between a static member lookup *with*
and *without* instance variables. The lookup without instance variables
corresponds to `find_name_in_mro`
[here](https://docs.python.org/3/howto/descriptor.html#invocation-from-an-instance).
We currently approximate both using `member_static`, which leads to two
open TODOs. Changing this would be a larger refactoring of
`Type::own_instance_member`, so I chose to leave it out of this PR.

## Test Plan

* New `call/methods.md` test suite for method calls
* New tests in `descriptor_protocol.md`
* New `call/getattr_static.md` test suite for `inspect.getattr_static`
* Various updated tests
2025-02-20 23:22:26 +01:00
David Peter
f62e5406f2 [red-knot] Short-circuit bool calls on bool (#16292)
## Summary

This avoids looking up `__bool__` on class `bool` for every
`Type::Instance(bool).bool()` call. 1% performance win on cold cache, 4%
win on incremental performance.
2025-02-20 23:06:11 +01:00
Douglas Creager
1be4394155 [red-knot] Consolidate SymbolBindings/SymbolDeclarations state (#16286)
This updates the `SymbolBindings` and `SymbolDeclarations` types to use
a single smallvec of live bindings/declarations, instead of splitting
that out into separate containers for each field.

I'm seeing an 11-13% `cargo bench` performance improvement with this
locally (for both cold and incremental). I'm interested to see if
Codspeed agrees!

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-02-20 16:20:23 -05:00
34 changed files with 1656 additions and 1347 deletions

148
Cargo.lock generated
View File

@@ -8,18 +8,6 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy 0.7.35",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
@@ -140,12 +128,6 @@ version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
[[package]]
name = "append-only-vec"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7992085ec035cfe96992dd31bfd495a2ebd31969bb95f624471cb6c0b349e571"
[[package]]
name = "arc-swap"
version = "1.7.1"
@@ -227,9 +209,12 @@ dependencies = [
[[package]]
name = "boxcar"
version = "0.2.8"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2721c3c5a6f0e7f7e607125d963fedeb765f545f67adc9d71ed934693881eb42"
checksum = "225450ee9328e1e828319b48a89726cffc1b0ad26fd9211ad435de9fa376acae"
dependencies = [
"loom",
]
[[package]]
name = "bstr"
@@ -1013,6 +998,19 @@ dependencies = [
"libc",
]
[[package]]
name = "generator"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd"
dependencies = [
"cfg-if",
"libc",
"log",
"rustversion",
"windows",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -1102,10 +1100,6 @@ name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]]
name = "hashbrown"
@@ -1113,17 +1107,18 @@ version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
[[package]]
name = "hashlink"
version = "0.9.1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
"hashbrown 0.14.5",
"hashbrown 0.15.2",
]
[[package]]
@@ -1179,7 +1174,7 @@ dependencies = [
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
"windows-core 0.52.0",
]
[[package]]
@@ -1683,6 +1678,19 @@ version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
[[package]]
name = "loom"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
dependencies = [
"cfg-if",
"generator",
"scoped-tls",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "lsp-server"
version = "0.7.8"
@@ -2814,6 +2822,7 @@ dependencies = [
"ruff_python_formatter",
"ruff_python_parser",
"ruff_python_trivia",
"ruff_server",
"ruff_workspace",
"schemars",
"serde",
@@ -3164,6 +3173,7 @@ dependencies = [
"ruff_diagnostics",
"ruff_formatter",
"ruff_linter",
"ruff_macros",
"ruff_notebook",
"ruff_python_ast",
"ruff_python_codegen",
@@ -3315,14 +3325,14 @@ checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
[[package]]
name = "salsa"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
source = "git+https://github.com/salsa-rs/salsa.git?rev=c8826fa4d1d9e3cba4c6e578763878b71fa9a10d#c8826fa4d1d9e3cba4c6e578763878b71fa9a10d"
dependencies = [
"append-only-vec",
"arc-swap",
"boxcar",
"compact_str",
"crossbeam",
"crossbeam-queue",
"dashmap 6.1.0",
"hashbrown 0.14.5",
"hashbrown 0.15.2",
"hashlink",
"indexmap",
"parking_lot",
@@ -3337,12 +3347,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.1.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
source = "git+https://github.com/salsa-rs/salsa.git?rev=c8826fa4d1d9e3cba4c6e578763878b71fa9a10d#c8826fa4d1d9e3cba4c6e578763878b71fa9a10d"
[[package]]
name = "salsa-macros"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
source = "git+https://github.com/salsa-rs/salsa.git?rev=c8826fa4d1d9e3cba4c6e578763878b71fa9a10d#c8826fa4d1d9e3cba4c6e578763878b71fa9a10d"
dependencies = [
"heck",
"proc-macro2",
@@ -3384,6 +3394,12 @@ dependencies = [
"syn 2.0.98",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -4451,6 +4467,16 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"windows-core 0.58.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.52.0"
@@ -4460,6 +4486,60 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-implement",
"windows-interface",
"windows-result",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-implement"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "windows-interface"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.48.0"

View File

@@ -123,7 +123,7 @@ rayon = { version = "1.10.0" }
regex = { version = "1.10.2" }
rustc-hash = { version = "2.0.0" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "351d9cf0037be949d17800d0c7b4838e533c2ed6" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "c8826fa4d1d9e3cba4c6e578763878b71fa9a10d" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }

View File

@@ -15,7 +15,7 @@ use crate::module_name::ModuleName;
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::ast_ids::AstIdsBuilder;
use crate::semantic_index::attribute_assignment::{AttributeAssignment, AttributeAssignments};
use crate::semantic_index::constraint::PatternConstraintKind;
use crate::semantic_index::constraint::{PatternConstraintKind, ScopedConstraintId};
use crate::semantic_index::definition::{
AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef, Definition, DefinitionNodeKey,
DefinitionNodeRef, ForStmtDefinitionNodeRef, ImportFromDefinitionNodeRef,
@@ -26,7 +26,7 @@ use crate::semantic_index::symbol::{
SymbolTableBuilder,
};
use crate::semantic_index::use_def::{
EagerBindingsKey, FlowSnapshot, ScopedConstraintId, ScopedEagerBindingsId, UseDefMapBuilder,
EagerBindingsKey, FlowSnapshot, ScopedEagerBindingsId, UseDefMapBuilder,
};
use crate::semantic_index::SemanticIndex;
use crate::unpack::{Unpack, UnpackValue};
@@ -294,7 +294,7 @@ impl<'db> SemanticIndexBuilder<'db> {
&self.use_def_maps[scope_id]
}
fn current_visibility_constraints_mut(&mut self) -> &mut VisibilityConstraintsBuilder<'db> {
fn current_visibility_constraints_mut(&mut self) -> &mut VisibilityConstraintsBuilder {
let scope_id = self.current_scope();
&mut self.use_def_maps[scope_id].visibility_constraints
}
@@ -406,16 +406,12 @@ impl<'db> SemanticIndexBuilder<'db> {
}
/// Negates a constraint and adds it to the list of all constraints, does not record it.
fn add_negated_constraint(
&mut self,
constraint: Constraint<'db>,
) -> (Constraint<'db>, ScopedConstraintId) {
fn add_negated_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
let negated = Constraint {
node: constraint.node,
is_positive: false,
};
let id = self.current_use_def_map_mut().add_constraint(negated);
(negated, id)
self.current_use_def_map_mut().add_constraint(negated)
}
/// Records a previously added constraint by adding it to all live bindings.
@@ -431,7 +427,7 @@ impl<'db> SemanticIndexBuilder<'db> {
/// Negates the given constraint and then adds it to all live bindings.
fn record_negated_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
let (_, id) = self.add_negated_constraint(constraint);
let id = self.add_negated_constraint(constraint);
self.record_constraint_id(id);
id
}
@@ -460,9 +456,10 @@ impl<'db> SemanticIndexBuilder<'db> {
&mut self,
constraint: Constraint<'db>,
) -> ScopedVisibilityConstraintId {
let constraint_id = self.current_use_def_map_mut().add_constraint(constraint);
let id = self
.current_visibility_constraints_mut()
.add_atom(constraint, 0);
.add_atom(constraint_id);
self.record_visibility_constraint_id(id);
id
}
@@ -1192,12 +1189,14 @@ where
// We need multiple copies of the visibility constraint for the while condition,
// since we need to model situations where the first evaluation of the condition
// returns True, but a later evaluation returns False.
let first_constraint_id = self.current_use_def_map_mut().add_constraint(constraint);
let later_constraint_id = self.current_use_def_map_mut().add_constraint(constraint);
let first_vis_constraint_id = self
.current_visibility_constraints_mut()
.add_atom(constraint, 0);
.add_atom(first_constraint_id);
let later_vis_constraint_id = self
.current_visibility_constraints_mut()
.add_atom(constraint, 1);
.add_atom(later_constraint_id);
// Save aside any break states from an outer loop
let saved_break_states = std::mem::take(&mut self.loop_break_states);
@@ -1778,13 +1777,13 @@ where
// anymore.
if index < values.len() - 1 {
let constraint = self.build_constraint(value);
let (constraint, constraint_id) = match op {
ast::BoolOp::And => (constraint, self.add_constraint(constraint)),
let constraint_id = match op {
ast::BoolOp::And => self.add_constraint(constraint),
ast::BoolOp::Or => self.add_negated_constraint(constraint),
};
let visibility_constraint = self
.current_visibility_constraints_mut()
.add_atom(constraint, 0);
.add_atom(constraint_id);
let after_expr = self.flow_snapshot();

View File

@@ -1,10 +1,40 @@
use ruff_db::files::File;
use ruff_index::{newtype_index, IndexVec};
use ruff_python_ast::Singleton;
use crate::db::Db;
use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{FileScopeId, ScopeId};
// A scoped identifier for each `Constraint` in a scope.
#[newtype_index]
#[derive(Ord, PartialOrd)]
pub(crate) struct ScopedConstraintId;
// A collection of constraints. This is currently stored in `UseDefMap`, which means we maintain a
// separate set of constraints for each scope in a file.
pub(crate) type Constraints<'db> = IndexVec<ScopedConstraintId, Constraint<'db>>;
#[derive(Debug, Default)]
pub(crate) struct ConstraintsBuilder<'db> {
constraints: IndexVec<ScopedConstraintId, Constraint<'db>>,
}
impl<'db> ConstraintsBuilder<'db> {
/// Adds a constraint. Note that we do not deduplicate constraints. If you add a `Constraint`
/// more than once, you will get distinct `ScopedConstraintId`s for each one. (This lets you
/// model constraint expressions that might evaluate to different values at different points of
/// execution.)
pub(crate) fn add_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
self.constraints.push(constraint)
}
pub(crate) fn build(mut self) -> Constraints<'db> {
self.constraints.shrink_to_fit();
self.constraints
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
pub(crate) struct Constraint<'db> {
pub(crate) node: ConstraintNode<'db>,

View File

@@ -165,7 +165,7 @@
//! 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
//! definitions (and constraints, in the case of bindings) in terms of [`ScopedDefinitionId`] and
//! [`ScopedConstraintId`], which are indices into the `all_definitions` and `all_constraints`
//! [`ScopedConstraintId`], which are indices into the `all_definitions` and `constraints`
//! indexvecs in the [`UseDefMap`].
//!
//! There is another special kind of possible "definition" for a symbol: there might be a path from
@@ -255,28 +255,27 @@
//! snapshot, and merging a snapshot into the current state. The logic using these methods lives in
//! [`SemanticIndexBuilder`](crate::semantic_index::builder::SemanticIndexBuilder), e.g. where it
//! visits a `StmtIf` node.
pub(crate) use self::symbol_state::ScopedConstraintId;
use self::symbol_state::{
BindingIdWithConstraintsIterator, ConstraintIdIterator, DeclarationIdIterator,
ScopedDefinitionId, SymbolBindings, SymbolDeclarations, SymbolState,
};
use crate::semantic_index::ast_ids::ScopedUseId;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::symbol::{FileScopeId, ScopedSymbolId};
use crate::semantic_index::use_def::symbol_state::DeclarationIdWithConstraint;
use crate::visibility_constraints::{
ScopedVisibilityConstraintId, VisibilityConstraints, VisibilityConstraintsBuilder,
};
use ruff_index::{newtype_index, IndexVec};
use rustc_hash::FxHashMap;
use super::constraint::Constraint;
use self::symbol_state::{
ConstraintIndexIterator, LiveBindingsIterator, LiveDeclaration, LiveDeclarationsIterator,
ScopedDefinitionId, SymbolBindings, SymbolDeclarations, SymbolState,
};
use crate::semantic_index::ast_ids::ScopedUseId;
use crate::semantic_index::constraint::{
Constraint, Constraints, ConstraintsBuilder, ScopedConstraintId,
};
use crate::semantic_index::definition::Definition;
use crate::semantic_index::symbol::{FileScopeId, ScopedSymbolId};
use crate::visibility_constraints::{
ScopedVisibilityConstraintId, VisibilityConstraints, VisibilityConstraintsBuilder,
};
mod bitset;
mod symbol_state;
type AllConstraints<'db> = IndexVec<ScopedConstraintId, Constraint<'db>>;
/// Applicable definitions and constraints for every use of a name.
#[derive(Debug, PartialEq, Eq, salsa::Update)]
pub(crate) struct UseDefMap<'db> {
@@ -285,10 +284,10 @@ pub(crate) struct UseDefMap<'db> {
all_definitions: IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
/// Array of [`Constraint`] in this scope.
all_constraints: AllConstraints<'db>,
constraints: Constraints<'db>,
/// Array of visibility constraints in this scope.
visibility_constraints: VisibilityConstraints<'db>,
visibility_constraints: VisibilityConstraints,
/// [`SymbolBindings`] reaching a [`ScopedUseId`].
bindings_by_use: IndexVec<ScopedUseId, SymbolBindings>,
@@ -370,7 +369,7 @@ impl<'db> UseDefMap<'db> {
) -> BindingWithConstraintsIterator<'map, 'db> {
BindingWithConstraintsIterator {
all_definitions: &self.all_definitions,
all_constraints: &self.all_constraints,
constraints: &self.constraints,
visibility_constraints: &self.visibility_constraints,
inner: bindings.iter(),
}
@@ -382,6 +381,7 @@ impl<'db> UseDefMap<'db> {
) -> DeclarationsIterator<'map, 'db> {
DeclarationsIterator {
all_definitions: &self.all_definitions,
constraints: &self.constraints,
visibility_constraints: &self.visibility_constraints,
inner: declarations.iter(),
}
@@ -415,26 +415,26 @@ type EagerBindings = IndexVec<ScopedEagerBindingsId, SymbolBindings>;
#[derive(Debug)]
pub(crate) struct BindingWithConstraintsIterator<'map, 'db> {
all_definitions: &'map IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
all_constraints: &'map AllConstraints<'db>,
pub(crate) visibility_constraints: &'map VisibilityConstraints<'db>,
inner: BindingIdWithConstraintsIterator<'map>,
pub(crate) constraints: &'map Constraints<'db>,
pub(crate) visibility_constraints: &'map VisibilityConstraints,
inner: LiveBindingsIterator<'map>,
}
impl<'map, 'db> Iterator for BindingWithConstraintsIterator<'map, 'db> {
type Item = BindingWithConstraints<'map, 'db>;
fn next(&mut self) -> Option<Self::Item> {
let all_constraints = self.all_constraints;
let constraints = self.constraints;
self.inner
.next()
.map(|binding_id_with_constraints| BindingWithConstraints {
binding: self.all_definitions[binding_id_with_constraints.definition],
constraints: ConstraintsIterator {
all_constraints,
constraint_ids: binding_id_with_constraints.constraint_ids,
.map(|live_binding| BindingWithConstraints {
binding: self.all_definitions[live_binding.binding],
narrowing_constraints: ConstraintsIterator {
constraints,
constraint_ids: live_binding.narrowing_constraints.iter(),
},
visibility_constraint: binding_id_with_constraints.visibility_constraint,
visibility_constraint: live_binding.visibility_constraint,
})
}
}
@@ -443,13 +443,13 @@ impl std::iter::FusedIterator for BindingWithConstraintsIterator<'_, '_> {}
pub(crate) struct BindingWithConstraints<'map, 'db> {
pub(crate) binding: Option<Definition<'db>>,
pub(crate) constraints: ConstraintsIterator<'map, 'db>,
pub(crate) narrowing_constraints: ConstraintsIterator<'map, 'db>,
pub(crate) visibility_constraint: ScopedVisibilityConstraintId,
}
pub(crate) struct ConstraintsIterator<'map, 'db> {
all_constraints: &'map AllConstraints<'db>,
constraint_ids: ConstraintIdIterator<'map>,
constraints: &'map Constraints<'db>,
constraint_ids: ConstraintIndexIterator<'map>,
}
impl<'db> Iterator for ConstraintsIterator<'_, 'db> {
@@ -458,7 +458,7 @@ impl<'db> Iterator for ConstraintsIterator<'_, 'db> {
fn next(&mut self) -> Option<Self::Item> {
self.constraint_ids
.next()
.map(|constraint_id| self.all_constraints[constraint_id])
.map(|constraint_id| self.constraints[ScopedConstraintId::from_u32(constraint_id)])
}
}
@@ -466,8 +466,9 @@ impl std::iter::FusedIterator for ConstraintsIterator<'_, '_> {}
pub(crate) struct DeclarationsIterator<'map, 'db> {
all_definitions: &'map IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
pub(crate) visibility_constraints: &'map VisibilityConstraints<'db>,
inner: DeclarationIdIterator<'map>,
pub(crate) constraints: &'map Constraints<'db>,
pub(crate) visibility_constraints: &'map VisibilityConstraints,
inner: LiveDeclarationsIterator<'map>,
}
pub(crate) struct DeclarationWithConstraint<'db> {
@@ -480,13 +481,13 @@ impl<'db> Iterator for DeclarationsIterator<'_, 'db> {
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(
|DeclarationIdWithConstraint {
definition,
|LiveDeclaration {
declaration,
visibility_constraint,
}| {
DeclarationWithConstraint {
declaration: self.all_definitions[definition],
visibility_constraint,
declaration: self.all_definitions[*declaration],
visibility_constraint: *visibility_constraint,
}
},
)
@@ -507,11 +508,11 @@ pub(super) struct UseDefMapBuilder<'db> {
/// Append-only array of [`Definition`].
all_definitions: IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
/// Append-only array of [`Constraint`].
all_constraints: AllConstraints<'db>,
/// Builder of constraints.
constraints: ConstraintsBuilder<'db>,
/// Builder of visibility constraints.
pub(super) visibility_constraints: VisibilityConstraintsBuilder<'db>,
pub(super) visibility_constraints: VisibilityConstraintsBuilder,
/// A constraint which describes the visibility of the unbound/undeclared state, i.e.
/// whether or not the start of the scope is visible. This is important for cases like
@@ -540,7 +541,7 @@ impl Default for UseDefMapBuilder<'_> {
fn default() -> Self {
Self {
all_definitions: IndexVec::from_iter([None]),
all_constraints: IndexVec::new(),
constraints: ConstraintsBuilder::default(),
visibility_constraints: VisibilityConstraintsBuilder::default(),
scope_start_visibility: ScopedVisibilityConstraintId::ALWAYS_TRUE,
bindings_by_use: IndexVec::new(),
@@ -573,7 +574,7 @@ impl<'db> UseDefMapBuilder<'db> {
}
pub(super) fn add_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
self.all_constraints.push(constraint)
self.constraints.add_constraint(constraint)
}
pub(super) fn record_constraint_id(&mut self, constraint: ScopedConstraintId) {
@@ -753,7 +754,6 @@ impl<'db> UseDefMapBuilder<'db> {
pub(super) fn finish(mut self) -> UseDefMap<'db> {
self.all_definitions.shrink_to_fit();
self.all_constraints.shrink_to_fit();
self.symbol_states.shrink_to_fit();
self.bindings_by_use.shrink_to_fit();
self.declarations_by_binding.shrink_to_fit();
@@ -762,7 +762,7 @@ impl<'db> UseDefMapBuilder<'db> {
UseDefMap {
all_definitions: self.all_definitions,
all_constraints: self.all_constraints,
constraints: self.constraints.build(),
visibility_constraints: self.visibility_constraints.build(),
bindings_by_use: self.bindings_by_use,
public_symbols: self.symbol_states,

View File

@@ -25,13 +25,6 @@ impl<const B: usize> Default for BitSet<B> {
}
impl<const B: usize> BitSet<B> {
/// Create and return a new [`BitSet`] with a single `value` inserted.
pub(super) fn with(value: u32) -> Self {
let mut bitset = Self::default();
bitset.insert(value);
bitset
}
/// Convert from Inline to Heap, if needed, and resize the Heap vector, if needed.
fn resize(&mut self, value: u32) {
let num_blocks_needed = (value / 64) + 1;
@@ -93,19 +86,6 @@ impl<const B: usize> BitSet<B> {
}
}
/// Union in-place with another [`BitSet`].
pub(super) fn union(&mut self, other: &BitSet<B>) {
let mut max_len = self.blocks().len();
let other_len = other.blocks().len();
if other_len > max_len {
max_len = other_len;
self.resize_blocks(max_len);
}
for (my_block, other_block) in self.blocks_mut().iter_mut().zip(other.blocks()) {
*my_block |= other_block;
}
}
/// Return an iterator over the values (in ascending order) in this [`BitSet`].
pub(super) fn iter(&self) -> BitSetIterator<'_, B> {
let blocks = self.blocks();
@@ -158,6 +138,15 @@ impl<const B: usize> std::iter::FusedIterator for BitSetIterator<'_, B> {}
mod tests {
use super::BitSet;
impl<const B: usize> BitSet<B> {
/// Create and return a new [`BitSet`] with a single `value` inserted.
pub(super) fn with(value: u32) -> Self {
let mut bitset = Self::default();
bitset.insert(value);
bitset
}
}
fn assert_bitset<const B: usize>(bitset: &BitSet<B>, contents: &[u32]) {
assert_eq!(bitset.iter().collect::<Vec<_>>(), contents);
}
@@ -235,59 +224,6 @@ mod tests {
assert_bitset(&b1, &[89]);
}
#[test]
fn union() {
let mut b1 = BitSet::<1>::with(2);
let b2 = BitSet::<1>::with(4);
b1.union(&b2);
assert_bitset(&b1, &[2, 4]);
}
#[test]
fn union_mixed_1() {
let mut b1 = BitSet::<1>::with(4);
let mut b2 = BitSet::<1>::with(4);
b1.insert(89);
b2.insert(5);
b1.union(&b2);
assert_bitset(&b1, &[4, 5, 89]);
}
#[test]
fn union_mixed_2() {
let mut b1 = BitSet::<1>::with(4);
let mut b2 = BitSet::<1>::with(4);
b1.insert(23);
b2.insert(89);
b1.union(&b2);
assert_bitset(&b1, &[4, 23, 89]);
}
#[test]
fn union_heap() {
let mut b1 = BitSet::<1>::with(4);
let mut b2 = BitSet::<1>::with(4);
b1.insert(89);
b2.insert(90);
b1.union(&b2);
assert_bitset(&b1, &[4, 89, 90]);
}
#[test]
fn union_heap_2() {
let mut b1 = BitSet::<1>::with(89);
let mut b2 = BitSet::<1>::with(89);
b1.insert(91);
b2.insert(90);
b1.union(&b2);
assert_bitset(&b1, &[89, 90, 91]);
}
#[test]
fn multiple_blocks() {
let mut b = BitSet::<2>::with(120);

View File

@@ -46,14 +46,16 @@
use itertools::{EitherOrBoth, Itertools};
use ruff_index::newtype_index;
use smallvec::SmallVec;
use smallvec::{smallvec, SmallVec};
use crate::semantic_index::constraint::ScopedConstraintId;
use crate::semantic_index::use_def::bitset::{BitSet, BitSetIterator};
use crate::semantic_index::use_def::VisibilityConstraintsBuilder;
use crate::visibility_constraints::ScopedVisibilityConstraintId;
/// A newtype-index for a definition in a particular scope.
#[newtype_index]
#[derive(Ord, PartialOrd)]
pub(super) struct ScopedDefinitionId;
impl ScopedDefinitionId {
@@ -65,89 +67,54 @@ impl ScopedDefinitionId {
pub(super) const UNBOUND: ScopedDefinitionId = ScopedDefinitionId::from_u32(0);
}
/// A newtype-index for a constraint expression in a particular scope.
#[newtype_index]
pub(crate) struct ScopedConstraintId;
/// Can reference this * 64 total definitions inline; more will fall back to the heap.
const INLINE_BINDING_BLOCKS: usize = 3;
/// A [`BitSet`] of [`ScopedDefinitionId`], representing live bindings of a symbol in a scope.
type Bindings = BitSet<INLINE_BINDING_BLOCKS>;
type BindingsIterator<'a> = BitSetIterator<'a, INLINE_BINDING_BLOCKS>;
/// Can reference this * 64 total declarations inline; more will fall back to the heap.
const INLINE_DECLARATION_BLOCKS: usize = 3;
/// A [`BitSet`] of [`ScopedDefinitionId`], representing live declarations of a symbol in a scope.
type Declarations = BitSet<INLINE_DECLARATION_BLOCKS>;
type DeclarationsIterator<'a> = BitSetIterator<'a, INLINE_DECLARATION_BLOCKS>;
/// Can reference this * 64 total constraints inline; more will fall back to the heap.
const INLINE_CONSTRAINT_BLOCKS: usize = 2;
/// Can keep inline this many live bindings per symbol at a given time; more will go to heap.
const INLINE_BINDINGS_PER_SYMBOL: usize = 4;
/// Can keep inline this many live bindings or declarations per symbol at a given time; more will
/// go to heap.
const INLINE_DEFINITIONS_PER_SYMBOL: usize = 4;
/// Which constraints apply to a given binding?
type Constraints = BitSet<INLINE_CONSTRAINT_BLOCKS>;
type InlineConstraintArray = [Constraints; INLINE_BINDINGS_PER_SYMBOL];
/// One [`BitSet`] of applicable [`ScopedConstraintId`]s per live binding.
type ConstraintsPerBinding = SmallVec<InlineConstraintArray>;
/// Iterate over all constraints for a single binding.
type ConstraintsIterator<'a> = std::slice::Iter<'a, Constraints>;
const INLINE_VISIBILITY_CONSTRAINTS: usize = 4;
type InlineVisibilityConstraintsArray =
[ScopedVisibilityConstraintId; INLINE_VISIBILITY_CONSTRAINTS];
/// One [`ScopedVisibilityConstraintId`] per live declaration.
type VisibilityConstraintPerDeclaration = SmallVec<InlineVisibilityConstraintsArray>;
/// One [`ScopedVisibilityConstraintId`] per live binding.
type VisibilityConstraintPerBinding = SmallVec<InlineVisibilityConstraintsArray>;
/// Iterator over the visibility constraints for all live bindings/declarations.
type VisibilityConstraintsIterator<'a> = std::slice::Iter<'a, ScopedVisibilityConstraintId>;
pub(super) type ConstraintIndexIterator<'a> = BitSetIterator<'a, INLINE_CONSTRAINT_BLOCKS>;
/// Live declarations for a single symbol at some point in control flow, with their
/// corresponding visibility constraints.
#[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update)]
pub(super) struct SymbolDeclarations {
/// [`BitSet`]: which declarations (as [`ScopedDefinitionId`]) can reach the current location?
///
/// Invariant: Because this is a `BitSet`, it can be viewed as a _sorted_ set of definition
/// IDs. The `visibility_constraints` field stores constraints for each definition. Therefore
/// those fields must always have the same `len()` as `live_declarations`, and the elements
/// must appear in the same order. Effectively, this means that elements must always be added
/// in sorted order, or via a binary search that determines the correct place to insert new
/// constraints.
pub(crate) live_declarations: Declarations,
/// For each live declaration, which visibility constraint applies to it?
pub(crate) visibility_constraints: VisibilityConstraintPerDeclaration,
/// A list of live declarations for this symbol, sorted by their `ScopedDefinitionId`
live_declarations: SmallVec<[LiveDeclaration; INLINE_DEFINITIONS_PER_SYMBOL]>,
}
/// One of the live declarations for a single symbol at some point in control flow.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct LiveDeclaration {
pub(super) declaration: ScopedDefinitionId,
pub(super) visibility_constraint: ScopedVisibilityConstraintId,
}
pub(super) type LiveDeclarationsIterator<'a> = std::slice::Iter<'a, LiveDeclaration>;
impl SymbolDeclarations {
fn undeclared(scope_start_visibility: ScopedVisibilityConstraintId) -> Self {
let initial_declaration = LiveDeclaration {
declaration: ScopedDefinitionId::UNBOUND,
visibility_constraint: scope_start_visibility,
};
Self {
live_declarations: Declarations::with(0),
visibility_constraints: VisibilityConstraintPerDeclaration::from_iter([
scope_start_visibility,
]),
live_declarations: smallvec![initial_declaration],
}
}
/// Record a newly-encountered declaration for this symbol.
fn record_declaration(&mut self, declaration_id: ScopedDefinitionId) {
self.live_declarations = Declarations::with(declaration_id.into());
self.visibility_constraints = VisibilityConstraintPerDeclaration::with_capacity(1);
self.visibility_constraints
.push(ScopedVisibilityConstraintId::ALWAYS_TRUE);
fn record_declaration(&mut self, declaration: ScopedDefinitionId) {
// The new declaration replaces all previous live declaration in this path.
self.live_declarations.clear();
self.live_declarations.push(LiveDeclaration {
declaration,
visibility_constraint: ScopedVisibilityConstraintId::ALWAYS_TRUE,
});
}
/// Add given visibility constraint to all live declarations.
@@ -156,45 +123,62 @@ impl SymbolDeclarations {
visibility_constraints: &mut VisibilityConstraintsBuilder,
constraint: ScopedVisibilityConstraintId,
) {
for existing in &mut self.visibility_constraints {
*existing = visibility_constraints.add_and_constraint(*existing, constraint);
for declaration in &mut self.live_declarations {
declaration.visibility_constraint = visibility_constraints
.add_and_constraint(declaration.visibility_constraint, constraint);
}
}
/// Return an iterator over live declarations for this symbol.
pub(super) fn iter(&self) -> DeclarationIdIterator {
DeclarationIdIterator {
declarations: self.live_declarations.iter(),
visibility_constraints: self.visibility_constraints.iter(),
pub(super) fn iter(&self) -> LiveDeclarationsIterator<'_> {
self.live_declarations.iter()
}
/// Iterate over the IDs of each currently live declaration for this symbol
fn iter_declarations(&self) -> impl Iterator<Item = ScopedDefinitionId> + '_ {
self.iter().map(|lb| lb.declaration)
}
fn simplify_visibility_constraints(&mut self, other: SymbolDeclarations) {
// 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())
{
return;
}
for (declaration, other_declaration) in self
.live_declarations
.iter_mut()
.zip(other.live_declarations)
{
declaration.visibility_constraint = other_declaration.visibility_constraint;
}
}
fn merge(&mut self, b: Self, visibility_constraints: &mut VisibilityConstraintsBuilder) {
let a = std::mem::take(self);
self.live_declarations = a.live_declarations.clone();
self.live_declarations.union(&b.live_declarations);
// Invariant: These zips are well-formed since we maintain an invariant that all of our
// fields are sets/vecs with the same length.
let a = (a.live_declarations.iter()).zip(a.visibility_constraints);
let b = (b.live_declarations.iter()).zip(b.visibility_constraints);
// Invariant: merge_join_by consumes the two iterators in sorted order, which ensures that
// the definition IDs and constraints line up correctly in the merged result. If a
// definition is found in both `a` and `b`, we compose the constraints from the two paths
// in an appropriate way (intersection for narrowing constraints; ternary OR for visibility
// constraints). If a definition is found in only one path, it is used as-is.
for zipped in a.merge_join_by(b, |(a_decl, _), (b_decl, _)| a_decl.cmp(b_decl)) {
// the merged `live_declarations` vec remains sorted. If a definition is found in both `a`
// and `b`, we compose the constraints from the two paths in an appropriate way
// (intersection for narrowing constraints; ternary OR for visibility constraints). If a
// definition is found in only one path, it is used as-is.
let a = a.live_declarations.into_iter();
let b = b.live_declarations.into_iter();
for zipped in a.merge_join_by(b, |a, b| a.declaration.cmp(&b.declaration)) {
match zipped {
EitherOrBoth::Both((_, a_vis_constraint), (_, b_vis_constraint)) => {
let vis_constraint = visibility_constraints
.add_or_constraint(a_vis_constraint, b_vis_constraint);
self.visibility_constraints.push(vis_constraint);
EitherOrBoth::Both(a, b) => {
let visibility_constraint = visibility_constraints
.add_or_constraint(a.visibility_constraint, b.visibility_constraint);
self.live_declarations.push(LiveDeclaration {
declaration: a.declaration,
visibility_constraint,
});
}
EitherOrBoth::Left((_, vis_constraint))
| EitherOrBoth::Right((_, vis_constraint)) => {
self.visibility_constraints.push(vis_constraint);
EitherOrBoth::Left(declaration) | EitherOrBoth::Right(declaration) => {
self.live_declarations.push(declaration);
}
}
}
@@ -205,57 +189,52 @@ impl SymbolDeclarations {
/// with a set of narrowing constraints and a visibility constraint.
#[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update)]
pub(super) struct SymbolBindings {
/// [`BitSet`]: which bindings (as [`ScopedDefinitionId`]) can reach the current location?
///
/// Invariant: Because this is a `BitSet`, it can be viewed as a _sorted_ set of definition
/// IDs. The `constraints` and `visibility_constraints` field stores constraints for each
/// definition. Therefore those fields must always have the same `len()` as
/// `live_bindings`, and the elements must appear in the same order. Effectively, this means
/// that elements must always be added in sorted order, or via a binary search that determines
/// the correct place to insert new constraints.
live_bindings: Bindings,
/// For each live binding, which [`ScopedConstraintId`] apply?
///
/// This is a [`smallvec::SmallVec`] which should always have one [`BitSet`] of constraints per
/// binding in `live_bindings`.
constraints: ConstraintsPerBinding,
/// For each live binding, which visibility constraint applies to it?
visibility_constraints: VisibilityConstraintPerBinding,
/// A list of live bindings for this symbol, sorted by their `ScopedDefinitionId`
live_bindings: SmallVec<[LiveBinding; INLINE_DEFINITIONS_PER_SYMBOL]>,
}
/// One of the live bindings for a single symbol at some point in control flow.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct LiveBinding {
pub(super) binding: ScopedDefinitionId,
pub(super) narrowing_constraints: Constraints,
pub(super) visibility_constraint: ScopedVisibilityConstraintId,
}
pub(super) type LiveBindingsIterator<'a> = std::slice::Iter<'a, LiveBinding>;
impl SymbolBindings {
fn unbound(scope_start_visibility: ScopedVisibilityConstraintId) -> Self {
let initial_binding = LiveBinding {
binding: ScopedDefinitionId::UNBOUND,
narrowing_constraints: Constraints::default(),
visibility_constraint: scope_start_visibility,
};
Self {
live_bindings: Bindings::with(ScopedDefinitionId::UNBOUND.as_u32()),
constraints: ConstraintsPerBinding::from_iter([Constraints::default()]),
visibility_constraints: VisibilityConstraintPerBinding::from_iter([
scope_start_visibility,
]),
live_bindings: smallvec![initial_binding],
}
}
/// Record a newly-encountered binding for this symbol.
pub(super) fn record_binding(
&mut self,
binding_id: ScopedDefinitionId,
binding: ScopedDefinitionId,
visibility_constraint: ScopedVisibilityConstraintId,
) {
// The new binding replaces all previous live bindings in this path, and has no
// constraints.
self.live_bindings = Bindings::with(binding_id.into());
self.constraints = ConstraintsPerBinding::with_capacity(1);
self.constraints.push(Constraints::default());
self.visibility_constraints = VisibilityConstraintPerBinding::with_capacity(1);
self.visibility_constraints.push(visibility_constraint);
self.live_bindings.clear();
self.live_bindings.push(LiveBinding {
binding,
narrowing_constraints: Constraints::default(),
visibility_constraint,
});
}
/// Add given constraint to all live bindings.
pub(super) fn record_constraint(&mut self, constraint_id: ScopedConstraintId) {
for bitset in &mut self.constraints {
bitset.insert(constraint_id.into());
for binding in &mut self.live_bindings {
binding.narrowing_constraints.insert(constraint_id.into());
}
}
@@ -265,71 +244,67 @@ impl SymbolBindings {
visibility_constraints: &mut VisibilityConstraintsBuilder,
constraint: ScopedVisibilityConstraintId,
) {
for existing in &mut self.visibility_constraints {
*existing = visibility_constraints.add_and_constraint(*existing, constraint);
for binding in &mut self.live_bindings {
binding.visibility_constraint = visibility_constraints
.add_and_constraint(binding.visibility_constraint, constraint);
}
}
/// Iterate over currently live bindings for this symbol
pub(super) fn iter(&self) -> BindingIdWithConstraintsIterator {
BindingIdWithConstraintsIterator {
definitions: self.live_bindings.iter(),
constraints: self.constraints.iter(),
visibility_constraints: self.visibility_constraints.iter(),
pub(super) fn iter(&self) -> LiveBindingsIterator<'_> {
self.live_bindings.iter()
}
/// Iterate over the IDs of each currently live binding for this symbol
fn iter_bindings(&self) -> impl Iterator<Item = ScopedDefinitionId> + '_ {
self.iter().map(|lb| lb.binding)
}
fn simplify_visibility_constraints(&mut self, other: SymbolBindings) {
// 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())
{
return;
}
for (binding, other_binding) in self.live_bindings.iter_mut().zip(other.live_bindings) {
binding.visibility_constraint = other_binding.visibility_constraint;
}
}
fn merge(&mut self, mut b: Self, visibility_constraints: &mut VisibilityConstraintsBuilder) {
let mut a = std::mem::take(self);
self.live_bindings = a.live_bindings.clone();
self.live_bindings.union(&b.live_bindings);
// Invariant: These zips are well-formed since we maintain an invariant that all of our
// fields are sets/vecs with the same length.
//
// Performance: We iterate over the `constraints` smallvecs via mut reference, because the
// individual elements are `BitSet`s (currently 24 bytes in size), and we don't want to
// move them by value multiple times during iteration. By iterating by reference, we only
// have to copy single pointers around. In the loop below, the `std::mem::take` calls
// specify precisely where we want to move them into the merged `constraints` smallvec.
//
// We don't need a similar optimization for `visibility_constraints`, since those elements
// are 32-bit IndexVec IDs, and so are already cheap to move/copy.
let a = (a.live_bindings.iter())
.zip(a.constraints.iter_mut())
.zip(a.visibility_constraints);
let b = (b.live_bindings.iter())
.zip(b.constraints.iter_mut())
.zip(b.visibility_constraints);
fn merge(&mut self, b: Self, visibility_constraints: &mut VisibilityConstraintsBuilder) {
let a = std::mem::take(self);
// Invariant: merge_join_by consumes the two iterators in sorted order, which ensures that
// the definition IDs and constraints line up correctly in the merged result. If a
// definition is found in both `a` and `b`, we compose the constraints from the two paths
// in an appropriate way (intersection for narrowing constraints; ternary OR for visibility
// constraints). If a definition is found in only one path, it is used as-is.
for zipped in a.merge_join_by(b, |((a_def, _), _), ((b_def, _), _)| a_def.cmp(b_def)) {
// the merged `live_bindings` vec remains sorted. If a definition is found in both `a` and
// `b`, we compose the constraints from the two paths in an appropriate way (intersection
// for narrowing constraints; ternary OR for visibility constraints). If a definition is
// found in only one path, it is used as-is.
let a = a.live_bindings.into_iter();
let b = b.live_bindings.into_iter();
for zipped in a.merge_join_by(b, |a, b| a.binding.cmp(&b.binding)) {
match zipped {
EitherOrBoth::Both(
((_, a_constraints), a_vis_constraint),
((_, b_constraints), b_vis_constraint),
) => {
EitherOrBoth::Both(a, b) => {
// If the same definition is visible through both paths, any constraint
// that applies on only one path is irrelevant to the resulting type from
// unioning the two paths, so we intersect the constraints.
let constraints = a_constraints;
constraints.intersect(b_constraints);
self.constraints.push(std::mem::take(constraints));
let mut narrowing_constraints = a.narrowing_constraints;
narrowing_constraints.intersect(&b.narrowing_constraints);
// For visibility constraints, we merge them using a ternary OR operation:
let vis_constraint = visibility_constraints
.add_or_constraint(a_vis_constraint, b_vis_constraint);
self.visibility_constraints.push(vis_constraint);
let visibility_constraint = visibility_constraints
.add_or_constraint(a.visibility_constraint, b.visibility_constraint);
self.live_bindings.push(LiveBinding {
binding: a.binding,
narrowing_constraints,
visibility_constraint,
});
}
EitherOrBoth::Left(((_, constraints), vis_constraint))
| EitherOrBoth::Right(((_, constraints), vis_constraint)) => {
self.constraints.push(std::mem::take(constraints));
self.visibility_constraints.push(vis_constraint);
EitherOrBoth::Left(binding) | EitherOrBoth::Right(binding) => {
self.live_bindings.push(binding);
}
}
}
@@ -379,14 +354,14 @@ impl SymbolState {
.record_visibility_constraint(visibility_constraints, constraint);
}
/// 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
/// changed.
pub(super) fn simplify_visibility_constraints(&mut self, snapshot_state: SymbolState) {
if self.bindings.live_bindings == snapshot_state.bindings.live_bindings {
self.bindings.visibility_constraints = snapshot_state.bindings.visibility_constraints;
}
if self.declarations.live_declarations == snapshot_state.declarations.live_declarations {
self.declarations.visibility_constraints =
snapshot_state.declarations.visibility_constraints;
}
self.bindings
.simplify_visibility_constraints(snapshot_state.bindings);
self.declarations
.simplify_visibility_constraints(snapshot_state.declarations);
}
/// Record a newly-encountered declaration of this symbol.
@@ -414,98 +389,6 @@ impl SymbolState {
}
}
/// A single binding (as [`ScopedDefinitionId`]) with an iterator of its applicable
/// narrowing constraints ([`ScopedConstraintId`]) and a corresponding visibility
/// visibility constraint ([`ScopedVisibilityConstraintId`]).
#[derive(Debug)]
pub(super) struct BindingIdWithConstraints<'map> {
pub(super) definition: ScopedDefinitionId,
pub(super) constraint_ids: ConstraintIdIterator<'map>,
pub(super) visibility_constraint: ScopedVisibilityConstraintId,
}
#[derive(Debug)]
pub(super) struct BindingIdWithConstraintsIterator<'map> {
definitions: BindingsIterator<'map>,
constraints: ConstraintsIterator<'map>,
visibility_constraints: VisibilityConstraintsIterator<'map>,
}
impl<'map> Iterator for BindingIdWithConstraintsIterator<'map> {
type Item = BindingIdWithConstraints<'map>;
fn next(&mut self) -> Option<Self::Item> {
match (
self.definitions.next(),
self.constraints.next(),
self.visibility_constraints.next(),
) {
(None, None, None) => None,
(Some(def), Some(constraints), Some(visibility_constraint_id)) => {
Some(BindingIdWithConstraints {
definition: ScopedDefinitionId::from_u32(def),
constraint_ids: ConstraintIdIterator {
wrapped: constraints.iter(),
},
visibility_constraint: *visibility_constraint_id,
})
}
// SAFETY: see above.
_ => unreachable!("definitions and constraints length mismatch"),
}
}
}
impl std::iter::FusedIterator for BindingIdWithConstraintsIterator<'_> {}
#[derive(Debug)]
pub(super) struct ConstraintIdIterator<'a> {
wrapped: BitSetIterator<'a, INLINE_CONSTRAINT_BLOCKS>,
}
impl Iterator for ConstraintIdIterator<'_> {
type Item = ScopedConstraintId;
fn next(&mut self) -> Option<Self::Item> {
self.wrapped.next().map(ScopedConstraintId::from_u32)
}
}
impl std::iter::FusedIterator for ConstraintIdIterator<'_> {}
/// A single declaration (as [`ScopedDefinitionId`]) with a corresponding visibility
/// visibility constraint ([`ScopedVisibilityConstraintId`]).
#[derive(Debug)]
pub(super) struct DeclarationIdWithConstraint {
pub(super) definition: ScopedDefinitionId,
pub(super) visibility_constraint: ScopedVisibilityConstraintId,
}
pub(super) struct DeclarationIdIterator<'map> {
pub(crate) declarations: DeclarationsIterator<'map>,
pub(crate) visibility_constraints: VisibilityConstraintsIterator<'map>,
}
impl Iterator for DeclarationIdIterator<'_> {
type Item = DeclarationIdWithConstraint;
fn next(&mut self) -> Option<Self::Item> {
match (self.declarations.next(), self.visibility_constraints.next()) {
(None, None) => None,
(Some(declaration), Some(&visibility_constraint)) => {
Some(DeclarationIdWithConstraint {
definition: ScopedDefinitionId::from_u32(declaration),
visibility_constraint,
})
}
// SAFETY: see above.
_ => unreachable!("declarations and visibility_constraints length mismatch"),
}
}
}
impl std::iter::FusedIterator for DeclarationIdIterator<'_> {}
#[cfg(test)]
mod tests {
use super::*;
@@ -515,16 +398,16 @@ mod tests {
let actual = symbol
.bindings()
.iter()
.map(|def_id_with_constraints| {
let def_id = def_id_with_constraints.definition;
.map(|live_binding| {
let def_id = live_binding.binding;
let def = if def_id == ScopedDefinitionId::UNBOUND {
"unbound".into()
} else {
def_id.as_u32().to_string()
};
let constraints = def_id_with_constraints
.constraint_ids
.map(ScopedConstraintId::as_u32)
let constraints = live_binding
.narrowing_constraints
.iter()
.map(|idx| idx.to_string())
.collect::<Vec<_>>()
.join(", ");
@@ -540,14 +423,14 @@ mod tests {
.declarations()
.iter()
.map(
|DeclarationIdWithConstraint {
definition,
|LiveDeclaration {
declaration,
visibility_constraint: _,
}| {
if definition == ScopedDefinitionId::UNBOUND {
if *declaration == ScopedDefinitionId::UNBOUND {
"undeclared".into()
} else {
definition.as_u32().to_string()
declaration.as_u32().to_string()
}
},
)

View File

@@ -1,10 +1,9 @@
use ruff_db::files::File;
use ruff_python_ast as ast;
use crate::module_resolver::file_to_module;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId};
use crate::semantic_index::{self, global_scope, use_def_map, DeclarationWithConstraint};
use crate::semantic_index::{global_scope, use_def_map, DeclarationWithConstraint};
use crate::semantic_index::{
symbol_table, BindingWithConstraints, BindingWithConstraintsIterator, DeclarationsIterator,
};
@@ -14,6 +13,8 @@ use crate::types::{
};
use crate::{resolve_module, Db, KnownModule, Module, Program};
pub(crate) use implicit_globals::module_type_implicit_global_symbol;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Boundness {
Bound,
@@ -183,20 +184,34 @@ pub(crate) fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> S
symbol_impl(db, scope, name, RequiresExplicitReExport::No)
}
/// Infers the public type of a module-global symbol as seen from within the same file.
/// Infers the public type of an explicit module-global symbol as seen from within the same file.
///
/// If it's not defined explicitly in the global scope, it will look it up in `types.ModuleType`
/// with a few very special exceptions.
/// 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
/// those additional symbols.
///
/// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports).
pub(crate) fn global_symbol<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
pub(crate) fn explicit_global_symbol<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
symbol_impl(
db,
global_scope(db, file),
name,
RequiresExplicitReExport::No,
)
.or_fall_back_to(db, || module_type_symbol(db, name))
}
/// Infers the public type of an explicit module-global symbol as seen from within the same file.
///
/// Unlike [`explicit_global_symbol`], this function also considers various "implicit globals"
/// such as `__name__`, `__doc__` and `__file__`. These are looked up as attributes on `types.ModuleType`
/// 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).
#[cfg(test)]
pub(crate) fn global_symbol<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
explicit_global_symbol(db, file, name)
.or_fall_back_to(db, || module_type_implicit_global_symbol(db, name))
}
/// Infers the public type of an imported symbol.
@@ -204,16 +219,16 @@ pub(crate) fn imported_symbol<'db>(db: &'db dyn Db, module: &Module, name: &str)
// If it's not found in the global scope, check if it's present as an instance on
// `types.ModuleType` or `builtins.object`.
//
// We do a more limited version of this in `global_symbol`, but there are two crucial
// differences here:
// We do a more limited version of this in `module_type_implicit_global_symbol`,
// but there are two crucial differences here:
// - If a member is looked up as an attribute, `__init__` is also available on the module, but
// it isn't available as a global from inside the module
// - If a member is looked up as an attribute, members on `builtins.object` are also available
// (because `types.ModuleType` inherits from `object`); these attributes are also not
// available as globals from inside the module.
//
// The same way as in `global_symbol`, however, we need to be careful to ignore
// `__getattr__`. Typeshed has a fake `__getattr__` on `types.ModuleType` to help out with
// The same way as in `module_type_implicit_global_symbol`, however, we need to be careful to
// ignore `__getattr__`. Typeshed has a fake `__getattr__` on `types.ModuleType` to help out with
// dynamic imports; we shouldn't use it for `ModuleLiteral` types where we know exactly which
// module we're dealing with.
external_symbol_impl(db, module.file(), name).or_fall_back_to(db, || {
@@ -239,7 +254,7 @@ pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db>
// We're looking up in the builtins namespace and not the module, so we should
// do the normal lookup in `types.ModuleType` and not the special one as in
// `imported_symbol`.
module_type_symbol(db, symbol)
module_type_implicit_global_symbol(db, symbol)
})
})
.unwrap_or(Symbol::Unbound)
@@ -488,6 +503,7 @@ fn symbol_from_bindings_impl<'db>(
bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>,
requires_explicit_reexport: RequiresExplicitReExport,
) -> Symbol<'db> {
let constraints = bindings_with_constraints.constraints;
let visibility_constraints = bindings_with_constraints.visibility_constraints;
let mut bindings_with_constraints = bindings_with_constraints.peekable();
@@ -499,9 +515,9 @@ fn symbol_from_bindings_impl<'db>(
Some(BindingWithConstraints {
binding,
visibility_constraint,
constraints: _,
narrowing_constraints: _,
}) if binding.map_or(true, is_non_exported) => {
visibility_constraints.evaluate(db, *visibility_constraint)
visibility_constraints.evaluate(db, constraints, *visibility_constraint)
}
_ => Truthiness::AlwaysFalse,
};
@@ -509,7 +525,7 @@ fn symbol_from_bindings_impl<'db>(
let mut types = bindings_with_constraints.filter_map(
|BindingWithConstraints {
binding,
constraints,
narrowing_constraints,
visibility_constraint,
}| {
let binding = binding?;
@@ -518,13 +534,14 @@ fn symbol_from_bindings_impl<'db>(
return None;
}
let static_visibility = visibility_constraints.evaluate(db, visibility_constraint);
let static_visibility =
visibility_constraints.evaluate(db, constraints, visibility_constraint);
if static_visibility.is_always_false() {
return None;
}
let mut constraint_tys = constraints
let mut constraint_tys = narrowing_constraints
.filter_map(|constraint| narrowing_constraint(db, constraint, binding))
.peekable();
@@ -575,6 +592,7 @@ fn symbol_from_declarations_impl<'db>(
declarations: DeclarationsIterator<'_, 'db>,
requires_explicit_reexport: RequiresExplicitReExport,
) -> SymbolFromDeclarationsResult<'db> {
let constraints = declarations.constraints;
let visibility_constraints = declarations.visibility_constraints;
let mut declarations = declarations.peekable();
@@ -587,7 +605,7 @@ fn symbol_from_declarations_impl<'db>(
declaration,
visibility_constraint,
}) if declaration.map_or(true, is_non_exported) => {
visibility_constraints.evaluate(db, *visibility_constraint)
visibility_constraints.evaluate(db, constraints, *visibility_constraint)
}
_ => Truthiness::AlwaysFalse,
};
@@ -603,7 +621,8 @@ fn symbol_from_declarations_impl<'db>(
return None;
}
let static_visibility = visibility_constraints.evaluate(db, visibility_constraint);
let static_visibility =
visibility_constraints.evaluate(db, constraints, visibility_constraint);
if static_visibility.is_always_false() {
None
@@ -658,63 +677,106 @@ fn symbol_from_declarations_impl<'db>(
}
}
/// Return a list of the symbols that typeshed declares in the body scope of
/// the stub for the class `types.ModuleType`.
///
/// Conceptually this could be a `Set` rather than a list,
/// but the number of symbols declared in this scope is likely to be very small,
/// so the cost of hashing the names is likely to be more expensive than it's worth.
#[salsa::tracked(return_ref)]
fn module_type_symbols<'db>(db: &'db dyn Db) -> smallvec::SmallVec<[ast::name::Name; 8]> {
let Some(module_type) = KnownClass::ModuleType
.to_class_literal(db)
.into_class_literal()
else {
// The most likely way we get here is if a user specified a `--custom-typeshed-dir`
// without a `types.pyi` stub in the `stdlib/` directory
return smallvec::SmallVec::default();
};
mod implicit_globals {
use ruff_python_ast as ast;
let module_type_scope = module_type.body_scope(db);
let module_type_symbol_table = symbol_table(db, module_type_scope);
use crate::db::Db;
use crate::semantic_index::{self, symbol_table};
use crate::types::KnownClass;
// `__dict__` and `__init__` are very special members that can be accessed as attributes
// on the module when imported, but cannot be accessed as globals *inside* the module.
//
// `__getattr__` is even more special: it doesn't exist at runtime, but typeshed includes it
// to reduce false positives associated with functions that dynamically import modules
// and return `Instance(types.ModuleType)`. We should ignore it for any known module-literal type.
module_type_symbol_table
.symbols()
.filter(|symbol| symbol.is_declared())
.map(semantic_index::symbol::Symbol::name)
.filter(|symbol_name| !matches!(&***symbol_name, "__dict__" | "__getattr__" | "__init__"))
.cloned()
.collect()
}
use super::Symbol;
/// Return the symbol for a member of `types.ModuleType`.
///
/// ## Notes
///
/// 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
/// type, since it has the same end result. The reason to only call [`member`] on [`ModuleType`]
/// instance when absolutely necessary is that it was a fairly significant performance regression
/// to fallback to doing that for every name lookup that wasn't found in the module's globals
/// ([`global_symbol`]). So we use less idiomatic (and much more verbose) code here as a
/// micro-optimisation because it's used in a very hot path.
///
/// [`member`]: Type::member
/// [`ModuleType`]: KnownClass::ModuleType
fn module_type_symbol<'db>(db: &'db dyn Db, name: &str) -> Symbol<'db> {
if module_type_symbols(db)
.iter()
.any(|module_type_member| &**module_type_member == name)
{
KnownClass::ModuleType.to_instance(db).member(db, name)
} else {
Symbol::Unbound
/// Looks up the type of an "implicit global symbol". Returns [`Symbol::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__`
/// that are implicitly defined in every module's global scope. Because their type is
/// always the same, we simply look these up as instance attributes on `types.ModuleType`.
///
/// Note that this function should only be used as a fallback if a symbol is being looked
/// 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
/// 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,
) -> Symbol<'db> {
// 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
// type, since it has the same end result. The reason to only call `.member()` on `ModuleType`
// when absolutely necessary is that this function is used in a very hot path (name resolution
// in `infer.rs`). We use less idiomatic (and much more verbose) code here as a micro-optimisation.
if module_type_symbols(db)
.iter()
.any(|module_type_member| &**module_type_member == name)
{
KnownClass::ModuleType.to_instance(db).member(db, name)
} else {
Symbol::Unbound
}
}
/// An internal micro-optimisation for `module_type_implicit_global_symbol`.
///
/// This function returns a list of the symbols that typeshed declares in the
/// body scope of the stub for the class `types.ModuleType`.
///
/// The returned list excludes the attributes `__dict__` and `__init__`. These are very
/// special members that can be accessed as attributes on the module when imported,
/// but cannot be accessed as globals *inside* the module.
///
/// The list also excludes `__getattr__`. `__getattr__` is even more special: it doesn't
/// exist at runtime, but typeshed includes it to reduce false positives associated with
/// functions that dynamically import modules and return `Instance(types.ModuleType)`.
/// We should ignore it for any known module-literal type.
///
/// Conceptually this function could be a `Set` rather than a list,
/// but the number of symbols declared in this scope is likely to be very small,
/// so the cost of hashing the names is likely to be more expensive than it's worth.
#[salsa::tracked(return_ref)]
fn module_type_symbols<'db>(db: &'db dyn Db) -> smallvec::SmallVec<[ast::name::Name; 8]> {
let Some(module_type) = KnownClass::ModuleType
.to_class_literal(db)
.into_class_literal()
else {
// The most likely way we get here is if a user specified a `--custom-typeshed-dir`
// without a `types.pyi` stub in the `stdlib/` directory
return smallvec::SmallVec::default();
};
let module_type_scope = module_type.body_scope(db);
let module_type_symbol_table = symbol_table(db, module_type_scope);
module_type_symbol_table
.symbols()
.filter(|symbol| symbol.is_declared())
.map(semantic_index::symbol::Symbol::name)
.filter(|symbol_name| {
!matches!(&***symbol_name, "__dict__" | "__getattr__" | "__init__")
})
.cloned()
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::tests::setup_db;
#[test]
fn module_type_symbols_includes_declared_types_but_not_referenced_types() {
let db = setup_db();
let symbol_names = module_type_symbols(&db);
let dunder_name_symbol_name = ast::name::Name::new_static("__name__");
assert!(symbol_names.contains(&dunder_name_symbol_name));
let property_symbol_name = ast::name::Name::new_static("property");
assert!(!symbol_names.contains(&property_symbol_name));
}
}
}
@@ -828,15 +890,36 @@ mod tests {
);
}
#[track_caller]
fn assert_bound_string_symbol<'db>(db: &'db dyn Db, symbol: Symbol<'db>) {
assert!(matches!(
symbol,
Symbol::Type(Type::Instance(_), Boundness::Bound)
));
assert_eq!(symbol.expect_type(), KnownClass::Str.to_instance(db));
}
#[test]
fn module_type_symbols_includes_declared_types_but_not_referenced_types() {
fn implicit_builtin_globals() {
let db = setup_db();
let symbol_names = module_type_symbols(&db);
assert_bound_string_symbol(&db, builtins_symbol(&db, "__name__"));
}
let dunder_name_symbol_name = ast::name::Name::new_static("__name__");
assert!(symbol_names.contains(&dunder_name_symbol_name));
#[test]
fn implicit_typing_globals() {
let db = setup_db();
assert_bound_string_symbol(&db, typing_symbol(&db, "__name__"));
}
let property_symbol_name = ast::name::Name::new_static("property");
assert!(!symbol_names.contains(&property_symbol_name));
#[test]
fn implicit_typing_extensions_globals() {
let db = setup_db();
assert_bound_string_symbol(&db, typing_extensions_symbol(&db, "__name__"));
}
#[test]
fn implicit_sys_globals() {
let db = setup_db();
assert_bound_string_symbol(&db, known_module_symbol(&db, KnownModule::Sys, "__name__"));
}
}

View File

@@ -33,8 +33,8 @@ use crate::semantic_index::{
};
use crate::suppression::check_suppressions;
use crate::symbol::{
global_symbol, imported_symbol, known_module_symbol, symbol, symbol_from_bindings,
symbol_from_declarations, Boundness, LookupError, LookupResult, Symbol, SymbolAndQualifiers,
imported_symbol, known_module_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
Boundness, LookupError, LookupResult, Symbol, SymbolAndQualifiers,
};
use crate::types::call::{bind_call, CallArguments, CallBinding, CallOutcome};
use crate::types::class_base::ClassBase;
@@ -1586,10 +1586,7 @@ impl<'db> Type<'db> {
.ignore_possibly_unbound()?
.try_call(
db,
CallArguments::positional(
db,
[instance.unwrap_or(Type::none(db)), owner],
),
&CallArguments::positional([instance.unwrap_or(Type::none(db)), owner]),
)
.map(|outcome| Some(outcome.return_type(db)))
.unwrap_or(None)
@@ -1705,7 +1702,9 @@ impl<'db> Type<'db> {
Type::AlwaysTruthy => Truthiness::AlwaysTrue,
Type::AlwaysFalsy => Truthiness::AlwaysFalse,
Type::Instance(InstanceType { class }) => {
if class.is_known(db, KnownClass::NoneType) {
if class.is_known(db, KnownClass::Bool) {
Truthiness::Ambiguous
} else if class.is_known(db, KnownClass::NoneType) {
Truthiness::AlwaysFalse
} else {
// We only check the `__bool__` method for truth testing, even though at
@@ -1713,7 +1712,7 @@ impl<'db> Type<'db> {
// and a subclass could add a `__bool__` method.
if let Ok(Type::BooleanLiteral(bool_val)) = self
.try_call_dunder(db, "__bool__", CallArguments::none(db))
.try_call_dunder(db, "__bool__", &CallArguments::none())
.map(|outcome| outcome.return_type(db))
{
bool_val.into()
@@ -1786,7 +1785,7 @@ impl<'db> Type<'db> {
return usize_len.try_into().ok().map(Type::IntLiteral);
}
let return_ty = match self.try_call_dunder(db, "__len__", CallArguments::none(db)) {
let return_ty = match self.try_call_dunder(db, "__len__", &CallArguments::none()) {
Ok(outcome) | Err(CallDunderError::PossiblyUnbound(outcome)) => outcome.return_type(db),
// TODO: emit a diagnostic
@@ -1802,377 +1801,359 @@ impl<'db> Type<'db> {
fn try_call(
self,
db: &'db dyn Db,
arguments: CallArguments<'db>,
arguments: &CallArguments<'_, 'db>,
) -> Result<CallOutcome<'db>, CallError<'db>> {
#[salsa::tracked]
fn try_call_query<'db>(
db: &'db dyn Db,
ty_self: Type<'db>,
arguments: CallArguments<'db>,
) -> Result<CallOutcome<'db>, CallError<'db>> {
match ty_self {
Type::Callable(CallableType::BoundMethod(bound_method)) => {
let instance = bound_method.self_instance(db);
let arguments = arguments.with_self(db, instance);
match self {
Type::Callable(CallableType::BoundMethod(bound_method)) => {
let instance = bound_method.self_instance(db);
let arguments = arguments.with_self(instance);
let binding = bind_call(
db,
arguments,
bound_method.function(db).signature(db),
ty_self,
);
let binding = bind_call(
db,
&arguments,
bound_method.function(db).signature(db),
self,
);
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
}
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
}
Type::Callable(CallableType::MethodWrapperDunderGet(function)) => {
// Here, we dynamically model the overloaded function signature of `types.FunctionType.__get__`.
// This is required because we need to return more precise types than what the signature in
// typeshed provides:
//
// ```py
// class FunctionType:
// # ...
// @overload
// def __get__(self, instance: None, owner: type, /) -> FunctionType: ...
// @overload
// def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ...
// ```
}
Type::Callable(CallableType::MethodWrapperDunderGet(function)) => {
// Here, we dynamically model the overloaded function signature of `types.FunctionType.__get__`.
// This is required because we need to return more precise types than what the signature in
// typeshed provides:
//
// ```py
// class FunctionType:
// # ...
// @overload
// def __get__(self, instance: None, owner: type, /) -> FunctionType: ...
// @overload
// def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ...
// ```
let first_argument_is_none = arguments
.first_argument(db)
.is_some_and(|ty| ty.is_none(db));
let first_argument_is_none =
arguments.first_argument().is_some_and(|ty| ty.is_none(db));
let signature = Signature::new(
Parameters::new([
Parameter::new(
Some("instance".into()),
Some(Type::object(db)),
ParameterKind::PositionalOnly { default_ty: None },
),
if first_argument_is_none {
Parameter::new(
Some("owner".into()),
Some(KnownClass::Type.to_instance(db)),
ParameterKind::PositionalOnly { default_ty: None },
)
} else {
Parameter::new(
Some("owner".into()),
Some(UnionType::from_elements(
db,
[KnownClass::Type.to_instance(db), Type::none(db)],
)),
ParameterKind::PositionalOnly {
default_ty: Some(Type::none(db)),
},
)
},
]),
Some(match arguments.first_argument(db) {
Some(ty) if ty.is_none(db) => Type::FunctionLiteral(function),
Some(instance) => Type::Callable(CallableType::BoundMethod(
BoundMethodType::new(db, function, instance),
)),
_ => Type::unknown(),
}),
);
let binding = bind_call(db, arguments, &signature, ty_self);
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
}
}
Type::Callable(CallableType::WrapperDescriptorDunderGet) => {
// Here, we also model `types.FunctionType.__get__`, but now we consider a call to
// this as a function, i.e. we also expect the `self` argument to be passed in.
let second_argument_is_none = arguments
.second_argument(db)
.is_some_and(|ty| ty.is_none(db));
let signature = Signature::new(
Parameters::new([
Parameter::new(
Some("self".into()),
Some(KnownClass::FunctionType.to_instance(db)),
ParameterKind::PositionalOnly { default_ty: None },
),
Parameter::new(
Some("instance".into()),
Some(Type::object(db)),
ParameterKind::PositionalOnly { default_ty: None },
),
if second_argument_is_none {
Parameter::new(
Some("owner".into()),
Some(KnownClass::Type.to_instance(db)),
ParameterKind::PositionalOnly { default_ty: None },
)
} else {
Parameter::new(
Some("owner".into()),
Some(UnionType::from_elements(
db,
[KnownClass::Type.to_instance(db), Type::none(db)],
)),
ParameterKind::PositionalOnly {
default_ty: Some(Type::none(db)),
},
)
},
]),
Some(
match (arguments.first_argument(db), arguments.second_argument(db)) {
(Some(function @ Type::FunctionLiteral(_)), Some(instance))
if instance.is_none(db) =>
{
function
}
(Some(Type::FunctionLiteral(function)), Some(instance)) => {
Type::Callable(CallableType::BoundMethod(BoundMethodType::new(
db, function, instance,
)))
}
_ => Type::unknown(),
},
let signature = Signature::new(
Parameters::new([
Parameter::new(
Some("instance".into()),
Some(Type::object(db)),
ParameterKind::PositionalOnly { default_ty: None },
),
);
if first_argument_is_none {
Parameter::new(
Some("owner".into()),
Some(KnownClass::Type.to_instance(db)),
ParameterKind::PositionalOnly { default_ty: None },
)
} else {
Parameter::new(
Some("owner".into()),
Some(UnionType::from_elements(
db,
[KnownClass::Type.to_instance(db), Type::none(db)],
)),
ParameterKind::PositionalOnly {
default_ty: Some(Type::none(db)),
},
)
},
]),
Some(match arguments.first_argument() {
Some(ty) if ty.is_none(db) => Type::FunctionLiteral(function),
Some(instance) => Type::Callable(CallableType::BoundMethod(
BoundMethodType::new(db, function, instance),
)),
_ => Type::unknown(),
}),
);
let binding = bind_call(db, arguments, &signature, ty_self);
let binding = bind_call(db, arguments, &signature, self);
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
}
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
}
Type::FunctionLiteral(function_type) => {
let mut binding =
bind_call(db, arguments, function_type.signature(db), ty_self);
}
Type::Callable(CallableType::WrapperDescriptorDunderGet) => {
// Here, we also model `types.FunctionType.__get__`, but now we consider a call to
// this as a function, i.e. we also expect the `self` argument to be passed in.
if binding.has_binding_errors() {
return Err(CallError::BindingError { binding });
let second_argument_is_none =
arguments.second_argument().is_some_and(|ty| ty.is_none(db));
let signature = Signature::new(
Parameters::new([
Parameter::new(
Some("self".into()),
Some(KnownClass::FunctionType.to_instance(db)),
ParameterKind::PositionalOnly { default_ty: None },
),
Parameter::new(
Some("instance".into()),
Some(Type::object(db)),
ParameterKind::PositionalOnly { default_ty: None },
),
if second_argument_is_none {
Parameter::new(
Some("owner".into()),
Some(KnownClass::Type.to_instance(db)),
ParameterKind::PositionalOnly { default_ty: None },
)
} else {
Parameter::new(
Some("owner".into()),
Some(UnionType::from_elements(
db,
[KnownClass::Type.to_instance(db), Type::none(db)],
)),
ParameterKind::PositionalOnly {
default_ty: Some(Type::none(db)),
},
)
},
]),
Some(
match (arguments.first_argument(), arguments.second_argument()) {
(Some(function @ Type::FunctionLiteral(_)), Some(instance))
if instance.is_none(db) =>
{
function
}
(Some(Type::FunctionLiteral(function)), Some(instance)) => {
Type::Callable(CallableType::BoundMethod(BoundMethodType::new(
db, function, instance,
)))
}
_ => Type::unknown(),
},
),
);
let binding = bind_call(db, arguments, &signature, self);
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
}
}
Type::FunctionLiteral(function_type) => {
let mut binding = bind_call(db, arguments, function_type.signature(db), self);
if binding.has_binding_errors() {
return Err(CallError::BindingError { binding });
}
match function_type.known(db) {
Some(KnownFunction::IsEquivalentTo) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding
.set_return_type(Type::BooleanLiteral(ty_a.is_equivalent_to(db, ty_b)));
}
Some(KnownFunction::IsSubtypeOf) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding.set_return_type(Type::BooleanLiteral(ty_a.is_subtype_of(db, ty_b)));
}
Some(KnownFunction::IsAssignableTo) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding
.set_return_type(Type::BooleanLiteral(ty_a.is_assignable_to(db, ty_b)));
}
Some(KnownFunction::IsDisjointFrom) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding
.set_return_type(Type::BooleanLiteral(ty_a.is_disjoint_from(db, ty_b)));
}
Some(KnownFunction::IsGradualEquivalentTo) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding.set_return_type(Type::BooleanLiteral(
ty_a.is_gradual_equivalent_to(db, ty_b),
));
}
Some(KnownFunction::IsFullyStatic) => {
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
binding.set_return_type(Type::BooleanLiteral(ty.is_fully_static(db)));
}
Some(KnownFunction::IsSingleton) => {
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
binding.set_return_type(Type::BooleanLiteral(ty.is_singleton(db)));
}
Some(KnownFunction::IsSingleValued) => {
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
binding.set_return_type(Type::BooleanLiteral(ty.is_single_valued(db)));
}
match function_type.known(db) {
Some(KnownFunction::IsEquivalentTo) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding.set_return_type(Type::BooleanLiteral(
ty_a.is_equivalent_to(db, ty_b),
));
}
Some(KnownFunction::IsSubtypeOf) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding.set_return_type(Type::BooleanLiteral(
ty_a.is_subtype_of(db, ty_b),
));
}
Some(KnownFunction::IsAssignableTo) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding.set_return_type(Type::BooleanLiteral(
ty_a.is_assignable_to(db, ty_b),
));
}
Some(KnownFunction::IsDisjointFrom) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding.set_return_type(Type::BooleanLiteral(
ty_a.is_disjoint_from(db, ty_b),
));
}
Some(KnownFunction::IsGradualEquivalentTo) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding.set_return_type(Type::BooleanLiteral(
ty_a.is_gradual_equivalent_to(db, ty_b),
));
}
Some(KnownFunction::IsFullyStatic) => {
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
binding.set_return_type(Type::BooleanLiteral(ty.is_fully_static(db)));
}
Some(KnownFunction::IsSingleton) => {
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
binding.set_return_type(Type::BooleanLiteral(ty.is_singleton(db)));
}
Some(KnownFunction::IsSingleValued) => {
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
binding.set_return_type(Type::BooleanLiteral(ty.is_single_valued(db)));
}
Some(KnownFunction::Len) => {
if let Some(first_arg) = binding.one_parameter_type() {
if let Some(len_ty) = first_arg.len(db) {
binding.set_return_type(len_ty);
}
};
}
Some(KnownFunction::Len) => {
if let Some(first_arg) = binding.one_parameter_type() {
if let Some(len_ty) = first_arg.len(db) {
binding.set_return_type(len_ty);
}
};
}
Some(KnownFunction::Repr) => {
if let Some(first_arg) = binding.one_parameter_type() {
binding.set_return_type(first_arg.repr(db));
};
}
Some(KnownFunction::Repr) => {
if let Some(first_arg) = binding.one_parameter_type() {
binding.set_return_type(first_arg.repr(db));
};
}
Some(KnownFunction::Cast) => {
// TODO: Use `.two_parameter_tys()` exclusively
// when overloads are supported.
if let Some(casted_ty) = arguments.first_argument() {
if binding.two_parameter_types().is_some() {
binding.set_return_type(casted_ty);
}
};
}
Some(KnownFunction::Cast) => {
// TODO: Use `.two_parameter_tys()` exclusively
// when overloads are supported.
if let Some(casted_ty) = arguments.first_argument(db) {
if binding.two_parameter_types().is_some() {
binding.set_return_type(casted_ty);
}
};
}
Some(KnownFunction::GetattrStatic) => {
let Some((instance_ty, attr_name, default)) =
binding.three_parameter_types()
else {
return Ok(CallOutcome::Single(binding));
};
Some(KnownFunction::GetattrStatic) => {
let Some((instance_ty, attr_name, default)) =
binding.three_parameter_types()
else {
return Ok(CallOutcome::Single(binding));
};
let Some(attr_name) = attr_name.into_string_literal() else {
return Ok(CallOutcome::Single(binding));
};
let Some(attr_name) = attr_name.into_string_literal() else {
return Ok(CallOutcome::Single(binding));
};
let default = if default.is_unknown() {
Type::Never
} else {
default
};
let default = if default.is_unknown() {
Type::Never
} else {
default
};
let union_with_default = |ty| UnionType::from_elements(db, [ty, default]);
let union_with_default =
|ty| UnionType::from_elements(db, [ty, default]);
// TODO: we could emit a diagnostic here (if default is not set)
binding.set_return_type(
match instance_ty.static_member(db, attr_name.value(db)) {
Symbol::Type(ty, Boundness::Bound) => {
if instance_ty.is_fully_static(db) {
ty
} else {
// Here, we attempt to model the fact that an attribute lookup on
// a non-fully static type could fail. This is an approximation,
// as there are gradual types like `tuple[Any]`, on which a lookup
// of (e.g. of the `index` method) would always succeed.
// TODO: we could emit a diagnostic here (if default is not set)
binding.set_return_type(
match instance_ty.static_member(db, attr_name.value(db)) {
Symbol::Type(ty, Boundness::Bound) => {
if instance_ty.is_fully_static(db) {
ty
} else {
// Here, we attempt to model the fact that an attribute lookup on
// a non-fully static type could fail. This is an approximation,
// as there are gradual types like `tuple[Any]`, on which a lookup
// of (e.g. of the `index` method) would always succeed.
union_with_default(ty)
}
}
Symbol::Type(ty, Boundness::PossiblyUnbound) => {
union_with_default(ty)
}
Symbol::Unbound => default,
},
);
}
_ => {}
};
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
}
}
// TODO annotated return type on `__new__` or metaclass `__call__`
// TODO check call vs signatures of `__new__` and/or `__init__`
Type::ClassLiteral(ClassLiteralType { class }) => {
Ok(CallOutcome::Single(CallBinding::from_return_type(
match class.known(db) {
// If the class is the builtin-bool class (for example `bool(1)`), we try to
// return the specific truthiness value of the input arg, `Literal[True]` for
// the example above.
Some(KnownClass::Bool) => arguments
.first_argument(db)
.map(|arg| arg.bool(db).into_type(db))
.unwrap_or(Type::BooleanLiteral(false)),
// TODO: Don't ignore the second and third arguments to `str`
// https://github.com/astral-sh/ruff/pull/16161#discussion_r1958425568
Some(KnownClass::Str) => arguments
.first_argument(db)
.map(|arg| arg.str(db))
.unwrap_or(Type::string_literal(db, "")),
_ => Type::Instance(InstanceType { class }),
},
)))
}
instance_ty @ Type::Instance(_) => {
instance_ty
.try_call_dunder(db, "__call__", arguments)
.map_err(|err| match err {
CallDunderError::Call(CallError::NotCallable { .. }) => {
// Turn "`<type of illegal '__call__'>` not callable" into
// "`X` not callable"
CallError::NotCallable {
not_callable_ty: ty_self,
}
}
CallDunderError::Call(CallError::Union {
called_ty: _,
bindings,
errors,
}) => CallError::Union {
called_ty: ty_self,
bindings,
errors,
Symbol::Type(ty, Boundness::PossiblyUnbound) => {
union_with_default(ty)
}
Symbol::Unbound => default,
},
CallDunderError::Call(error) => error,
// Turn "possibly unbound object of type `Literal['__call__']`"
// into "`X` not callable (possibly unbound `__call__` method)"
CallDunderError::PossiblyUnbound(outcome) => {
CallError::PossiblyUnboundDunderCall {
called_type: ty_self,
outcome: Box::new(outcome),
}
}
CallDunderError::MethodNotAvailable => {
// Turn "`X.__call__` unbound" into "`X` not callable"
CallError::NotCallable {
not_callable_ty: ty_self,
}
}
})
);
}
_ => {}
};
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
}
// Dynamic types are callable, and the return type is the same dynamic type
Type::Dynamic(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(ty_self))),
Type::Union(union) => CallOutcome::try_call_union(db, union, |element| {
element.try_call(db, arguments)
}),
Type::Intersection(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(
todo_type!("Type::Intersection.call()"),
))),
_ => Err(CallError::NotCallable {
not_callable_ty: ty_self,
}),
}
}
try_call_query(db, self, arguments)
// TODO annotated return type on `__new__` or metaclass `__call__`
// TODO check call vs signatures of `__new__` and/or `__init__`
Type::ClassLiteral(ClassLiteralType { class }) => {
Ok(CallOutcome::Single(CallBinding::from_return_type(
match class.known(db) {
// If the class is the builtin-bool class (for example `bool(1)`), we try to
// return the specific truthiness value of the input arg, `Literal[True]` for
// the example above.
Some(KnownClass::Bool) => arguments
.first_argument()
.map(|arg| arg.bool(db).into_type(db))
.unwrap_or(Type::BooleanLiteral(false)),
// TODO: Don't ignore the second and third arguments to `str`
// https://github.com/astral-sh/ruff/pull/16161#discussion_r1958425568
Some(KnownClass::Str) => arguments
.first_argument()
.map(|arg| arg.str(db))
.unwrap_or(Type::string_literal(db, "")),
_ => Type::Instance(InstanceType { class }),
},
)))
}
instance_ty @ Type::Instance(_) => {
instance_ty
.try_call_dunder(db, "__call__", arguments)
.map_err(|err| match err {
CallDunderError::Call(CallError::NotCallable { .. }) => {
// Turn "`<type of illegal '__call__'>` not callable" into
// "`X` not callable"
CallError::NotCallable {
not_callable_ty: self,
}
}
CallDunderError::Call(CallError::Union {
called_ty: _,
bindings,
errors,
}) => CallError::Union {
called_ty: self,
bindings,
errors,
},
CallDunderError::Call(error) => error,
// Turn "possibly unbound object of type `Literal['__call__']`"
// into "`X` not callable (possibly unbound `__call__` method)"
CallDunderError::PossiblyUnbound(outcome) => {
CallError::PossiblyUnboundDunderCall {
called_type: self,
outcome: Box::new(outcome),
}
}
CallDunderError::MethodNotAvailable => {
// Turn "`X.__call__` unbound" into "`X` not callable"
CallError::NotCallable {
not_callable_ty: self,
}
}
})
}
// Dynamic types are callable, and the return type is the same dynamic type
Type::Dynamic(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(self))),
Type::Union(union) => {
CallOutcome::try_call_union(db, union, |element| element.try_call(db, arguments))
}
Type::Intersection(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(
todo_type!("Type::Intersection.call()"),
))),
_ => Err(CallError::NotCallable {
not_callable_ty: self,
}),
}
}
/// Return the outcome of calling an class/instance attribute of this type
@@ -2185,13 +2166,13 @@ impl<'db> Type<'db> {
self,
db: &'db dyn Db,
receiver_ty: &Type<'db>,
arguments: CallArguments<'db>,
arguments: &CallArguments<'_, 'db>,
) -> Result<CallOutcome<'db>, CallError<'db>> {
match self {
Type::FunctionLiteral(..) => {
// Functions are always descriptors, so this would effectively call
// the function with the instance as the first argument
self.try_call(db, arguments.with_self(db, *receiver_ty))
self.try_call(db, &arguments.with_self(*receiver_ty))
}
Type::Instance(_) | Type::ClassLiteral(_) => self.try_call(db, arguments),
@@ -2218,7 +2199,7 @@ impl<'db> Type<'db> {
self,
db: &'db dyn Db,
name: &str,
arguments: CallArguments<'db>,
arguments: &CallArguments<'_, 'db>,
) -> Result<CallOutcome<'db>, CallDunderError<'db>> {
match self.to_meta_type(db).member(db, name) {
Symbol::Type(callable_ty, Boundness::Bound) => {
@@ -2247,12 +2228,12 @@ impl<'db> Type<'db> {
};
}
let dunder_iter_result = self.try_call_dunder(db, "__iter__", CallArguments::none(db));
let dunder_iter_result = self.try_call_dunder(db, "__iter__", &CallArguments::none());
match &dunder_iter_result {
Ok(outcome) | Err(CallDunderError::PossiblyUnbound(outcome)) => {
let iterator_ty = outcome.return_type(db);
return match iterator_ty.try_call_dunder(db, "__next__", CallArguments::none(db)) {
return match iterator_ty.try_call_dunder(db, "__next__", &CallArguments::none()) {
Ok(outcome) => {
if matches!(
dunder_iter_result,
@@ -2300,7 +2281,7 @@ impl<'db> Type<'db> {
match self.try_call_dunder(
db,
"__getitem__",
CallArguments::positional(db, [KnownClass::Int.to_instance(db)]),
&CallArguments::positional([KnownClass::Int.to_instance(db)]),
) {
Ok(outcome) => IterationOutcome::Iterable {
element_ty: outcome.return_type(db),
@@ -4169,9 +4150,9 @@ impl<'db> Class<'db> {
let namespace = KnownClass::Dict.to_instance(db);
// TODO: Other keyword arguments?
let arguments = CallArguments::positional(db, [name, bases, namespace]);
let arguments = CallArguments::positional([name, bases, namespace]);
let return_ty_result = match metaclass.try_call(db, arguments) {
let return_ty_result = match metaclass.try_call(db, &arguments) {
Ok(outcome) => Ok(outcome.return_type(db)),
Err(CallError::NotCallable { not_callable_ty }) => Err(MetaclassError {
@@ -5103,7 +5084,7 @@ static_assertions::assert_eq_size!(Type, [u8; 16]);
pub(crate) mod tests {
use super::*;
use crate::db::tests::{setup_db, TestDbBuilder};
use crate::symbol::{typing_extensions_symbol, typing_symbol};
use crate::symbol::{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::DbWithTestSystem;

View File

@@ -11,7 +11,7 @@ pub(super) use bind::{bind_call, CallBinding};
/// A successfully bound call where all arguments are valid.
///
/// It's guaranteed that the wrapped bindings have no errors.
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum CallOutcome<'db> {
/// The call resolves to exactly one binding.
Single(CallBinding<'db>),
@@ -84,7 +84,7 @@ impl<'db> CallOutcome<'db> {
}
/// The reason why calling a type failed.
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum CallError<'db> {
/// The type is not callable.
NotCallable {

View File

@@ -1,60 +1,63 @@
use ruff_python_ast::name::Name;
use crate::Db;
use super::Type;
/// Typed arguments for a single call, in source order.
#[salsa::interned]
pub(crate) struct CallArguments<'db> {
args: Vec<Argument<'db>>,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct CallArguments<'a, 'db>(Vec<Argument<'a, 'db>>);
impl<'a, 'db> CallArguments<'db> {
impl<'a, 'db> CallArguments<'a, 'db> {
/// Create a [`CallArguments`] with no arguments.
pub(crate) fn none(db: &'db dyn Db) -> Self {
CallArguments::new(db, Vec::new())
pub(crate) fn none() -> Self {
Self(Vec::new())
}
/// Create a [`CallArguments`] from an iterator over non-variadic positional argument types.
pub(crate) fn positional(
db: &'db dyn Db,
positional_tys: impl IntoIterator<Item = Type<'db>>,
) -> Self {
CallArguments::new(
db,
positional_tys
.into_iter()
.map(Argument::Positional)
.collect::<Vec<_>>(),
)
pub(crate) fn positional(positional_tys: impl IntoIterator<Item = Type<'db>>) -> Self {
positional_tys
.into_iter()
.map(Argument::Positional)
.collect()
}
/// Prepend an extra positional argument.
pub(crate) fn with_self(&self, db: &'db dyn Db, self_ty: Type<'db>) -> Self {
let mut arguments = Vec::with_capacity(self.args(db).len() + 1);
pub(crate) fn with_self(&self, self_ty: Type<'db>) -> Self {
let mut arguments = Vec::with_capacity(self.0.len() + 1);
arguments.push(Argument::Synthetic(self_ty));
arguments.extend_from_slice(&self.args(db));
CallArguments::new(db, arguments)
arguments.extend_from_slice(&self.0);
Self(arguments)
}
pub(crate) fn args_iter(&self, db: &'db dyn Db) -> impl IntoIterator<Item = Argument<'db>> {
self.args(db).into_iter()
pub(crate) fn iter(&self) -> impl Iterator<Item = &Argument<'a, 'db>> {
self.0.iter()
}
// TODO this should be eliminated in favor of [`bind_call`]
pub(crate) fn first_argument(&self, db: &'db dyn Db) -> Option<Type<'db>> {
self.args(db).first().map(Argument::ty)
pub(crate) fn first_argument(&self) -> Option<Type<'db>> {
self.0.first().map(Argument::ty)
}
// TODO this should be eliminated in favor of [`bind_call`]
pub(crate) fn second_argument(&self, db: &'db dyn Db) -> Option<Type<'db>> {
self.args(db).get(1).map(Argument::ty)
pub(crate) fn second_argument(&self) -> Option<Type<'db>> {
self.0.get(1).map(Argument::ty)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
pub(crate) enum Argument<'db> {
impl<'db, 'a, 'b> IntoIterator for &'b CallArguments<'a, 'db> {
type Item = &'b Argument<'a, 'db>;
type IntoIter = std::slice::Iter<'b, Argument<'a, 'db>>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl<'a, 'db> FromIterator<Argument<'a, 'db>> for CallArguments<'a, 'db> {
fn from_iter<T: IntoIterator<Item = Argument<'a, 'db>>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
#[derive(Clone, Debug)]
pub(crate) enum Argument<'a, 'db> {
/// The synthetic `self` or `cls` argument, which doesn't appear explicitly at the call site.
Synthetic(Type<'db>),
/// A positional argument.
@@ -62,12 +65,12 @@ pub(crate) enum Argument<'db> {
/// A starred positional argument (e.g. `*args`).
Variadic(Type<'db>),
/// A keyword argument (e.g. `a=1`).
Keyword { name: Name, ty: Type<'db> },
Keyword { name: &'a str, ty: Type<'db> },
/// The double-starred keywords argument (e.g. `**kwargs`).
Keywords(Type<'db>),
}
impl<'db> Argument<'db> {
impl<'db> Argument<'_, 'db> {
fn ty(&self) -> Type<'db> {
match self {
Self::Synthetic(ty) => *ty,

View File

@@ -16,7 +16,7 @@ use ruff_text_size::Ranged;
/// parameters, and any errors resulting from binding the call.
pub(crate) fn bind_call<'db>(
db: &'db dyn Db,
arguments: CallArguments<'db>,
arguments: &CallArguments<'_, 'db>,
signature: &Signature<'db>,
callable_ty: Type<'db>,
) -> CallBinding<'db> {
@@ -38,7 +38,7 @@ pub(crate) fn bind_call<'db>(
None
}
};
for (argument_index, argument) in arguments.args_iter(db).into_iter().enumerate() {
for (argument_index, argument) in arguments.iter().enumerate() {
let (index, parameter, argument_ty, positional) = match argument {
Argument::Positional(ty) | Argument::Synthetic(ty) => {
if matches!(argument, Argument::Synthetic(_)) {
@@ -58,7 +58,7 @@ pub(crate) fn bind_call<'db>(
}
Argument::Keyword { name, ty } => {
let Some((index, parameter)) = parameters
.keyword_by_name(&name)
.keyword_by_name(name)
.or_else(|| parameters.keyword_variadic())
else {
errors.push(CallBindingError::UnknownArgument {
@@ -81,13 +81,13 @@ pub(crate) fn bind_call<'db>(
parameter: ParameterContext::new(parameter, index, positional),
argument_index: get_argument_index(argument_index, num_synthetic_args),
expected_ty,
provided_ty: argument_ty,
provided_ty: *argument_ty,
});
}
}
if let Some(existing) = parameter_tys[index].replace(argument_ty) {
if let Some(existing) = parameter_tys[index].replace(*argument_ty) {
if parameter.is_variadic() || parameter.is_keyword_variadic() {
let union = UnionType::from_elements(db, [existing, argument_ty]);
let union = UnionType::from_elements(db, [existing, *argument_ty]);
parameter_tys[index].replace(union);
} else {
errors.push(CallBindingError::ParameterAlreadyAssigned {
@@ -137,7 +137,7 @@ pub(crate) fn bind_call<'db>(
}
}
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CallBinding<'db> {
/// Type of the callable object (function, class...)
callable_ty: Type<'db>,
@@ -273,7 +273,7 @@ impl std::fmt::Display for ParameterContexts {
}
}
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum CallBindingError<'db> {
/// The type of an argument is not assignable to the annotated type of its corresponding
/// parameter.

View File

@@ -50,7 +50,8 @@ use crate::semantic_index::semantic_index;
use crate::semantic_index::symbol::{FileScopeId, NodeWithScopeKind, NodeWithScopeRef, ScopeId};
use crate::semantic_index::SemanticIndex;
use crate::symbol::{
builtins_module_scope, builtins_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
builtins_module_scope, builtins_symbol, explicit_global_symbol,
module_type_implicit_global_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
typing_extensions_symbol, LookupError,
};
use crate::types::call::{Argument, CallArguments};
@@ -90,7 +91,7 @@ use super::slots::check_class_slots;
use super::string_annotation::{
parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION,
};
use super::{global_symbol, CallDunderError, ParameterExpectation, ParameterExpectations};
use super::{CallDunderError, ParameterExpectation, ParameterExpectations};
/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope.
/// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
@@ -1646,7 +1647,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
let target_ty = enter_ty
.try_call(self.db(), CallArguments::positional(self.db(), [context_expression_ty]))
.try_call(self.db(), &CallArguments::positional([context_expression_ty]))
.map(|outcome| outcome.return_type(self.db()))
.unwrap_or_else(|err| {
// TODO: Use more specific error messages for the different error cases.
@@ -1691,15 +1692,12 @@ impl<'db> TypeInferenceBuilder<'db> {
if exit_ty
.try_call(
self.db(),
CallArguments::positional(
self.db(),
[
context_manager_ty,
Type::none(self.db()),
Type::none(self.db()),
Type::none(self.db()),
],
),
&CallArguments::positional([
context_manager_ty,
Type::none(self.db()),
Type::none(self.db()),
Type::none(self.db()),
]),
)
.is_err()
{
@@ -2244,7 +2242,7 @@ impl<'db> TypeInferenceBuilder<'db> {
{
let call = class_member.try_call(
self.db(),
CallArguments::positional(self.db(), [target_type, value_type]),
&CallArguments::positional([target_type, value_type]),
);
let augmented_return_ty = match call {
Ok(t) => t.return_type(self.db()),
@@ -2725,53 +2723,46 @@ impl<'db> TypeInferenceBuilder<'db> {
&mut self,
arguments: &'a ast::Arguments,
parameter_expectations: ParameterExpectations,
) -> CallArguments<'db> {
CallArguments::new(
self.db(),
arguments
.arguments_source_order()
.enumerate()
.map(|(index, arg_or_keyword)| {
let infer_argument_type =
match parameter_expectations.expectation_at_index(index) {
ParameterExpectation::TypeExpression => Self::infer_type_expression,
ParameterExpectation::ValueExpression => Self::infer_expression,
};
) -> CallArguments<'a, 'db> {
arguments
.arguments_source_order()
.enumerate()
.map(|(index, arg_or_keyword)| {
let infer_argument_type = match parameter_expectations.expectation_at_index(index) {
ParameterExpectation::TypeExpression => Self::infer_type_expression,
ParameterExpectation::ValueExpression => Self::infer_expression,
};
match arg_or_keyword {
ast::ArgOrKeyword::Arg(arg) => match arg {
ast::Expr::Starred(ast::ExprStarred {
value,
range: _,
ctx: _,
}) => {
let ty = infer_argument_type(self, value);
self.store_expression_type(arg, ty);
Argument::Variadic(ty)
}
// TODO diagnostic if after a keyword argument
_ => Argument::Positional(infer_argument_type(self, arg)),
},
ast::ArgOrKeyword::Keyword(ast::Keyword {
arg,
match arg_or_keyword {
ast::ArgOrKeyword::Arg(arg) => match arg {
ast::Expr::Starred(ast::ExprStarred {
value,
range: _,
ctx: _,
}) => {
let ty = infer_argument_type(self, value);
if let Some(arg) = arg {
Argument::Keyword {
name: arg.id.clone(),
ty,
}
} else {
// TODO diagnostic if not last
Argument::Keywords(ty)
}
self.store_expression_type(arg, ty);
Argument::Variadic(ty)
}
// TODO diagnostic if after a keyword argument
_ => Argument::Positional(infer_argument_type(self, arg)),
},
ast::ArgOrKeyword::Keyword(ast::Keyword {
arg,
value,
range: _,
}) => {
let ty = infer_argument_type(self, value);
if let Some(arg) = arg {
Argument::Keyword { name: &arg.id, ty }
} else {
// TODO diagnostic if not last
Argument::Keywords(ty)
}
}
})
.collect::<Vec<_>>(),
)
}
})
.collect()
}
fn infer_optional_expression(&mut self, expression: Option<&ast::Expr>) -> Option<Type<'db>> {
@@ -3287,7 +3278,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.unwrap_or_default();
let call_arguments = self.infer_arguments(arguments, parameter_expectations);
let call = function_type.try_call(self.db(), call_arguments);
let call = function_type.try_call(self.db(), &call_arguments);
match call {
Ok(outcome) => {
@@ -3581,7 +3572,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
Symbol::Unbound
// No nonlocal binding? Check the module's globals.
// 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() {
@@ -3598,8 +3589,12 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
global_symbol(db, self.file(), symbol_name)
explicit_global_symbol(db, self.file(), symbol_name)
})
// 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))
// Not found in globals? Fallback to builtins
// (without infinite recursion if we're already in builtins.)
.or_fall_back_to(db, || {
@@ -3791,7 +3786,7 @@ impl<'db> TypeInferenceBuilder<'db> {
match operand_type.try_call_dunder(
self.db(),
unary_dunder_method,
CallArguments::none(self.db()),
&CallArguments::none(),
) {
Ok(outcome) => outcome.return_type(self.db()),
Err(e) => {
@@ -4042,7 +4037,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.try_call_dunder(
self.db(),
reflected_dunder,
CallArguments::positional(self.db(), [left_ty]),
&CallArguments::positional([left_ty]),
)
.map(|outcome| outcome.return_type(self.db()))
.or_else(|_| {
@@ -4050,7 +4045,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.try_call_dunder(
self.db(),
op.dunder(),
CallArguments::positional(self.db(), [right_ty]),
&CallArguments::positional([right_ty]),
)
.map(|outcome| outcome.return_type(self.db()))
})
@@ -4063,10 +4058,7 @@ impl<'db> TypeInferenceBuilder<'db> {
left_class.member(self.db(), op.dunder())
{
class_member
.try_call(
self.db(),
CallArguments::positional(self.db(), [left_ty, right_ty]),
)
.try_call(self.db(), &CallArguments::positional([left_ty, right_ty]))
.map(|outcome| outcome.return_type(self.db()))
.ok()
} else {
@@ -4084,7 +4076,7 @@ impl<'db> TypeInferenceBuilder<'db> {
class_member
.try_call(
self.db(),
CallArguments::positional(self.db(), [right_ty, left_ty]),
&CallArguments::positional([right_ty, left_ty]),
)
.map(|outcome| outcome.return_type(self.db()))
.ok()
@@ -4653,23 +4645,21 @@ impl<'db> TypeInferenceBuilder<'db> {
let db = self.db();
// The following resource has details about the rich comparison algorithm:
// https://snarky.ca/unravelling-rich-comparison-operators/
let call_dunder =
|op: RichCompareOperator, left: InstanceType<'db>, right: InstanceType<'db>| {
// TODO: How do we want to handle possibly unbound dunder methods?
match left.class.class_member(db, op.dunder()) {
Symbol::Type(class_member_dunder, Boundness::Bound) => class_member_dunder
.try_call(
db,
CallArguments::positional(
db,
[Type::Instance(left), Type::Instance(right)],
),
)
.map(|outcome| outcome.return_type(db))
.ok(),
_ => None,
}
};
let call_dunder = |op: RichCompareOperator,
left: InstanceType<'db>,
right: InstanceType<'db>| {
// TODO: How do we want to handle possibly unbound dunder methods?
match left.class.class_member(db, op.dunder()) {
Symbol::Type(class_member_dunder, Boundness::Bound) => class_member_dunder
.try_call(
db,
&CallArguments::positional([Type::Instance(left), Type::Instance(right)]),
)
.map(|outcome| outcome.return_type(db))
.ok(),
_ => None,
}
};
// The reflected dunder has priority if the right-hand side is a strict subclass of the left-hand side.
if left != right && right.is_subtype_of(db, left) {
@@ -4713,10 +4703,7 @@ impl<'db> TypeInferenceBuilder<'db> {
contains_dunder
.try_call(
db,
CallArguments::positional(
db,
[Type::Instance(right), Type::Instance(left)],
),
&CallArguments::positional([Type::Instance(right), Type::Instance(left)]),
)
.map(|outcome| outcome.return_type(db))
.ok()
@@ -4974,7 +4961,7 @@ impl<'db> TypeInferenceBuilder<'db> {
match value_ty.try_call_dunder(
self.db(),
"__getitem__",
CallArguments::positional(self.db(), [slice_ty]),
&CallArguments::positional([slice_ty]),
) {
Ok(outcome) => return outcome.return_type(self.db()),
Err(err @ CallDunderError::PossiblyUnbound { .. }) => {
@@ -5035,7 +5022,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
return ty
.try_call(self.db(), CallArguments::positional(self.db(),[value_ty, slice_ty]))
.try_call(self.db(), &CallArguments::positional([value_ty, slice_ty]))
.map(|outcome| outcome.return_type(self.db()))
.unwrap_or_else(|err| {
self.context.report_lint(
@@ -6266,6 +6253,7 @@ mod tests {
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::types::check_types;
use ruff_db::files::{system_path_to_file, File};
use ruff_db::system::DbWithTestSystem;

View File

@@ -345,7 +345,8 @@ pub(crate) enum ParameterKind<'db> {
mod tests {
use super::*;
use crate::db::tests::{setup_db, TestDb};
use crate::types::{global_symbol, FunctionType, KnownClass};
use crate::symbol::global_symbol;
use crate::types::{FunctionType, KnownClass};
use ruff_db::system::DbWithTestSystem;
#[track_caller]

View File

@@ -178,7 +178,9 @@ use std::cmp::Ordering;
use ruff_index::{Idx, IndexVec};
use rustc_hash::FxHashMap;
use crate::semantic_index::constraint::{Constraint, ConstraintNode, PatternConstraintKind};
use crate::semantic_index::constraint::{
Constraint, ConstraintNode, Constraints, PatternConstraintKind, ScopedConstraintId,
};
use crate::types::{infer_expression_type, Truthiness};
use crate::Db;
@@ -231,69 +233,15 @@ impl std::fmt::Debug for ScopedVisibilityConstraintId {
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
struct InteriorNode {
atom: Atom,
/// A "variable" that is evaluated as part of a TDD ternary function. For visibility
/// constraints, this is a `Constraint` that represents some runtime property of the Python
/// code that we are evaluating.
atom: ScopedConstraintId,
if_true: ScopedVisibilityConstraintId,
if_ambiguous: ScopedVisibilityConstraintId,
if_false: ScopedVisibilityConstraintId,
}
/// A "variable" that is evaluated as part of a TDD ternary function. For visibility constraints,
/// this is a `Constraint` that represents some runtime property of the Python code that we are
/// evaluating. We intern these constraints in an arena ([`VisibilityConstraints::constraints`]).
/// An atom is then an index into this arena.
///
/// By using a 32-bit index, we would typically allow 4 billion distinct constraints within a
/// scope. However, we sometimes have to model how a `Constraint` can have a different runtime
/// value at different points in the execution of the program. To handle this, we reserve the top
/// byte of an atom to represent a "copy number". This is just an opaque value that allows
/// different `Atom`s to evaluate the same `Constraint`. This yields a maximum of 16 million
/// distinct `Constraint`s in a scope, and 256 possible copies of each of those constraints.
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
struct Atom(u32);
impl Atom {
/// Deconstruct an atom into a constraint index and a copy number.
#[inline]
fn into_index_and_copy(self) -> (u32, u8) {
let copy = self.0 >> 24;
let index = self.0 & 0x00ff_ffff;
(index, copy as u8)
}
#[inline]
fn copy_of(mut self, copy: u8) -> Self {
// Clear out the previous copy number
self.0 &= 0x00ff_ffff;
// OR in the new one
self.0 |= u32::from(copy) << 24;
self
}
}
// A custom Debug implementation that prints out the constraint index and copy number as distinct
// fields.
impl std::fmt::Debug for Atom {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (index, copy) = self.into_index_and_copy();
f.debug_tuple("Atom").field(&index).field(&copy).finish()
}
}
impl Idx for Atom {
#[inline]
fn new(value: usize) -> Self {
assert!(value <= 0x00ff_ffff);
#[allow(clippy::cast_possible_truncation)]
Self(value as u32)
}
#[inline]
fn index(self) -> usize {
let (index, _) = self.into_index_and_copy();
index as usize
}
}
impl ScopedVisibilityConstraintId {
/// A special ID that is used for an "always true" / "always visible" constraint.
pub(crate) const ALWAYS_TRUE: ScopedVisibilityConstraintId =
@@ -336,16 +284,13 @@ const SMALLEST_TERMINAL: ScopedVisibilityConstraintId = ALWAYS_FALSE;
/// A collection of visibility constraints. This is currently stored in `UseDefMap`, which means we
/// maintain a separate set of visibility constraints for each scope in file.
#[derive(Debug, PartialEq, Eq, salsa::Update)]
pub(crate) struct VisibilityConstraints<'db> {
constraints: IndexVec<Atom, Constraint<'db>>,
pub(crate) struct VisibilityConstraints {
interiors: IndexVec<ScopedVisibilityConstraintId, InteriorNode>,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct VisibilityConstraintsBuilder<'db> {
constraints: IndexVec<Atom, Constraint<'db>>,
pub(crate) struct VisibilityConstraintsBuilder {
interiors: IndexVec<ScopedVisibilityConstraintId, InteriorNode>,
constraint_cache: FxHashMap<Constraint<'db>, Atom>,
interior_cache: FxHashMap<InteriorNode, ScopedVisibilityConstraintId>,
not_cache: FxHashMap<ScopedVisibilityConstraintId, ScopedVisibilityConstraintId>,
and_cache: FxHashMap<
@@ -358,10 +303,9 @@ pub(crate) struct VisibilityConstraintsBuilder<'db> {
>,
}
impl<'db> VisibilityConstraintsBuilder<'db> {
pub(crate) fn build(self) -> VisibilityConstraints<'db> {
impl VisibilityConstraintsBuilder {
pub(crate) fn build(self) -> VisibilityConstraints {
VisibilityConstraints {
constraints: self.constraints,
interiors: self.interiors,
}
}
@@ -385,14 +329,6 @@ impl<'db> VisibilityConstraintsBuilder<'db> {
}
}
/// Adds a constraint, ensuring that we only store any particular constraint once.
fn add_constraint(&mut self, constraint: Constraint<'db>, copy: u8) -> Atom {
self.constraint_cache
.entry(constraint)
.or_insert_with(|| self.constraints.push(constraint))
.copy_of(copy)
}
/// Adds an interior node, ensuring that we always use the same visibility constraint ID for
/// equal nodes.
fn add_interior(&mut self, node: InteriorNode) -> ScopedVisibilityConstraintId {
@@ -408,17 +344,23 @@ impl<'db> VisibilityConstraintsBuilder<'db> {
.or_insert_with(|| self.interiors.push(node))
}
/// Adds a new visibility constraint that checks a single [`Constraint`]. Provide different
/// values for `copy` if you need to model that the constraint can evaluate to different
/// results at different points in the execution of the program being modeled.
/// Adds a new visibility constraint that checks a single [`Constraint`].
///
/// [`ScopedConstraintId`]s are the “variables” that are evaluated by a TDD. A TDD variable has
/// the same value no matter how many times it appears in the ternary formula that the TDD
/// represents.
///
/// However, we sometimes have to model how a `Constraint` can have a different runtime
/// value at different points in the execution of the program. To handle this, you can take
/// advantage of the fact that the [`Constraints`] arena does not deduplicate `Constraint`s.
/// You can add a `Constraint` multiple times, yielding different `ScopedConstraintId`s, which
/// you can then create separate TDD atoms for.
pub(crate) fn add_atom(
&mut self,
constraint: Constraint<'db>,
copy: u8,
constraint: ScopedConstraintId,
) -> ScopedVisibilityConstraintId {
let atom = self.add_constraint(constraint, copy);
self.add_interior(InteriorNode {
atom,
atom: constraint,
if_true: ALWAYS_TRUE,
if_ambiguous: AMBIGUOUS,
if_false: ALWAYS_FALSE,
@@ -588,11 +530,12 @@ impl<'db> VisibilityConstraintsBuilder<'db> {
}
}
impl<'db> VisibilityConstraints<'db> {
impl VisibilityConstraints {
/// Analyze the statically known visibility for a given visibility constraint.
pub(crate) fn evaluate(
pub(crate) fn evaluate<'db>(
&self,
db: &'db dyn Db,
constraints: &Constraints<'db>,
mut id: ScopedVisibilityConstraintId,
) -> Truthiness {
loop {
@@ -602,7 +545,7 @@ impl<'db> VisibilityConstraints<'db> {
ALWAYS_FALSE => return Truthiness::AlwaysFalse,
_ => self.interiors[id],
};
let constraint = &self.constraints[node.atom];
let constraint = &constraints[node.atom];
match Self::analyze_single(db, constraint) {
Truthiness::AlwaysTrue => id = node.if_true,
Truthiness::Ambiguous => id = node.if_ambiguous,

View File

@@ -22,6 +22,7 @@ ruff_python_codegen = { workspace = true }
ruff_python_formatter = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_server = { workspace = true }
ruff_workspace = { workspace = true, features = ["schemars"] }
anyhow = { workspace = true }

View File

@@ -2,6 +2,7 @@
//!
//! Used for <https://docs.astral.sh/ruff/settings/>.
use itertools::Itertools;
use ruff_server::ClientSettings;
use std::fmt::Write;
use ruff_python_trivia::textwrap;
@@ -15,12 +16,32 @@ pub(crate) fn generate() -> String {
&mut output,
Set::Toplevel(Options::metadata()),
&mut Vec::new(),
SetKind::Ruff,
);
output
}
fn generate_set(output: &mut String, set: Set, parents: &mut Vec<Set>) {
pub(crate) fn generate_server_options() -> String {
let mut output = String::new();
generate_set(
&mut output,
Set::Toplevel(ClientSettings::metadata()),
&mut Vec::new(),
SetKind::RuffServer,
);
output
}
#[derive(Copy, Clone)]
enum SetKind {
Ruff,
RuffServer,
}
fn generate_set(output: &mut String, set: Set, parents: &mut Vec<Set>, set_kind: SetKind) {
match &set {
Set::Toplevel(_) => {
output.push_str("### Top-level\n");
@@ -53,7 +74,7 @@ fn generate_set(output: &mut String, set: Set, parents: &mut Vec<Set>) {
// Generate the fields.
for (name, field) in &fields {
emit_field(output, name, field, parents.as_slice());
emit_field(output, name, field, parents.as_slice(), set_kind);
output.push_str("---\n\n");
}
@@ -66,6 +87,7 @@ fn generate_set(output: &mut String, set: Set, parents: &mut Vec<Set>) {
set: *sub_set,
},
parents,
set_kind,
);
}
@@ -93,7 +115,13 @@ impl Set {
}
}
fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[Set]) {
fn emit_field(
output: &mut String,
name: &str,
field: &OptionField,
parents: &[Set],
set_kind: SetKind,
) {
let header_level = if parents.is_empty() { "####" } else { "#####" };
let parents_anchor = parents.iter().filter_map(|parent| parent.name()).join("_");
@@ -137,28 +165,46 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S
output.push_str(&format!("**Type**: `{}`\n", field.value_type));
output.push('\n');
output.push_str("**Example usage**:\n\n");
output.push_str(&format_tab(
"pyproject.toml",
&format_header(field.scope, parents, ConfigurationFile::PyprojectToml),
field.example,
));
output.push_str(&format_tab(
"ruff.toml",
&format_header(field.scope, parents, ConfigurationFile::RuffToml),
field.example,
));
match set_kind {
SetKind::Ruff => {
output.push_str(&format_tab(
"pyproject.toml",
&format_content(field, parents, ConfigurationFile::PyprojectToml),
));
output.push_str(&format_tab(
"ruff.toml",
&format_content(field, parents, ConfigurationFile::RuffToml),
));
}
SetKind::RuffServer => {}
}
output.push('\n');
}
fn format_tab(tab_name: &str, header: &str, content: &str) -> String {
fn format_tab(tab_name: &str, content: &str) -> String {
format!(
"=== \"{}\"\n\n ```toml\n {}\n{}\n ```\n",
"=== \"{}\"\n\n{}\n\n",
tab_name,
header,
textwrap::indent(content, " ")
)
}
fn format_content(
field: &OptionField,
parents: &[Set],
configuration: ConfigurationFile,
) -> String {
let header = format_header(field.scope, parents, configuration);
format!(
"```toml\n{}\n{}\n```",
header,
textwrap::indent(field.example, " ")
)
}
/// Format the TOML header for the example usage for a given option.
///
/// For example: `[tool.ruff.format]` or `[tool.ruff.lint.isort]`.
@@ -187,6 +233,15 @@ enum ConfigurationFile {
RuffToml,
}
fn format_server_content(field: &OptionField, editor: Editor) -> String {}
#[derive(Debug, Copy, Clone)]
enum Editor {
VSCode,
Neovim,
Zed,
}
#[derive(Default)]
struct CollectOptionsVisitor {
groups: Vec<(String, OptionSet)>,

View File

@@ -46,6 +46,8 @@ enum Command {
GenerateRulesTable,
/// Generate a Markdown-compatible listing of configuration options.
GenerateOptions,
/// Generate a Markdown-compatible listing of server options.
GenerateServerOptions,
/// Generate CLI help.
GenerateCliHelp(generate_cli_help::Args),
/// Generate Markdown docs.
@@ -89,6 +91,9 @@ fn main() -> Result<ExitCode> {
Command::GenerateKnotSchema(args) => generate_knot_schema::main(&args)?,
Command::GenerateRulesTable => println!("{}", generate_rules_table::generate()),
Command::GenerateOptions => println!("{}", generate_options::generate()),
Command::GenerateServerOptions => {
println!("{}", generate_options::generate_server_options())
}
Command::GenerateCliHelp(args) => generate_cli_help::main(&args)?,
Command::GenerateDocs(args) => generate_docs::main(&args)?,
Command::PrintAST(args) => print_ast::main(&args)?,

View File

@@ -68,8 +68,44 @@ class TestClass:
def __eq__(self, **kwargs): # ignore **kwargs
...
def __eq__(self, /, other=42): # ignore positional-only args
def __eq__(self, /, other=42): # support positional-only args
...
def __eq__(self, *, other=42): # ignore positional-only args
def __eq__(self, *, other=42): # ignore keyword-only args
...
def __cmp__(self): # #16217 assert non-special method is skipped, expects 2 parameters
...
def __div__(self): # #16217 assert non-special method is skipped, expects 2 parameters
...
def __nonzero__(self, x): # #16217 assert non-special method is skipped, expects 1 parameter
...
def __unicode__(self, x): # #16217 assert non-special method is skipped, expects 1 parameter
...
def __next__(self, x): # #16217 assert special method is linted, expects 1 parameter
...
def __buffer__(self): # #16217 assert special method is linted, expects 2 parameters
...
def __class_getitem__(self): # #16217 assert special method is linted, expects 2 parameters
...
def __mro_entries__(self): # #16217 assert special method is linted, expects 2 parameters
...
def __release_buffer__(self): # #16217 assert special method is linted, expects 2 parameters
...
def __subclasshook__(self): # #16217 assert special method is linted, expects 2 parameters
...
def __setattr__(self, /, name): # #16217 assert positional-only special method is linted, expects 3 parameters
...
def __setitem__(self, key, /, value, extra_value): # #16217 assert positional-only special method is linted, expects 3 parameters
...

View File

@@ -81,28 +81,42 @@ class H(BaseModel):
final_variable: Final[list[int]] = []
from pydantic.v1 import BaseModel as V1BaseModel
class I(V1BaseModel):
mutable_default: list[int] = []
from pydantic.v1.generics import GenericModel
class J(GenericModel):
mutable_default: list[int] = []
def sqlmodel_import_checker():
from sqlmodel.main import SQLModel
class I(SQLModel):
class K(SQLModel):
id: int
mutable_default: list[int] = []
from sqlmodel import SQLModel
class J(SQLModel):
class L(SQLModel):
id: int
name: str
class K(SQLModel):
class M(SQLModel):
id: int
i_s: list[J] = []
class L(SQLModel):
class N(SQLModel):
id: int
i_j: list[K] = list()
i_j: list[L] = list()
# Lint should account for deferred annotations
# See https://github.com/astral-sh/ruff/issues/15857

View File

@@ -23,10 +23,8 @@ impl ExpectedParams {
| "__neg__" | "__pos__" | "__abs__" | "__invert__" | "__complex__" | "__int__"
| "__float__" | "__index__" | "__trunc__" | "__floor__" | "__ceil__" | "__enter__"
| "__aenter__" | "__getnewargs_ex__" | "__getnewargs__" | "__getstate__"
| "__reduce__" | "__copy__" | "__unicode__" | "__nonzero__" | "__await__"
| "__aiter__" | "__anext__" | "__fspath__" | "__subclasses__" => {
Some(ExpectedParams::Fixed(0))
}
| "__reduce__" | "__copy__" | "__await__" | "__aiter__" | "__anext__"
| "__fspath__" | "__subclasses__" | "__next__" => Some(ExpectedParams::Fixed(0)),
"__format__" | "__lt__" | "__le__" | "__eq__" | "__ne__" | "__gt__" | "__ge__"
| "__getattr__" | "__getattribute__" | "__delattr__" | "__delete__"
| "__instancecheck__" | "__subclasscheck__" | "__getitem__" | "__missing__"
@@ -37,8 +35,9 @@ impl ExpectedParams {
| "__rpow__" | "__rlshift__" | "__rrshift__" | "__rand__" | "__rxor__" | "__ror__"
| "__iadd__" | "__isub__" | "__imul__" | "__itruediv__" | "__ifloordiv__"
| "__imod__" | "__ilshift__" | "__irshift__" | "__iand__" | "__ixor__" | "__ior__"
| "__ipow__" | "__setstate__" | "__reduce_ex__" | "__deepcopy__" | "__cmp__"
| "__matmul__" | "__rmatmul__" | "__imatmul__" | "__div__" => {
| "__ipow__" | "__setstate__" | "__reduce_ex__" | "__deepcopy__" | "__matmul__"
| "__rmatmul__" | "__imatmul__" | "__buffer__" | "__class_getitem__"
| "__mro_entries__" | "__release_buffer__" | "__subclasshook__" => {
Some(ExpectedParams::Fixed(1))
}
"__setattr__" | "__get__" | "__set__" | "__setitem__" | "__set_name__" => {
@@ -147,11 +146,8 @@ pub(crate) fn unexpected_special_method_signature(
return;
}
// Ignore methods with positional-only or keyword-only parameters, or variadic parameters.
if !parameters.posonlyargs.is_empty()
|| !parameters.kwonlyargs.is_empty()
|| parameters.kwarg.is_some()
{
// Ignore methods with keyword-only parameters or variadic parameters.
if !parameters.kwonlyargs.is_empty() || parameters.kwarg.is_some() {
return;
}
@@ -160,10 +156,11 @@ pub(crate) fn unexpected_special_method_signature(
return;
}
let actual_params = parameters.args.len();
let actual_params = parameters.args.len() + parameters.posonlyargs.len();
let mandatory_params = parameters
.args
.iter()
.chain(parameters.posonlyargs.iter())
.filter(|arg| arg.default.is_none())
.count();

View File

@@ -71,3 +71,75 @@ unexpected_special_method_signature.py:65:9: PLE0302 The special method `__round
| ^^^^^^^^^ PLE0302
66 | ...
|
unexpected_special_method_signature.py:89:9: PLE0302 The special method `__next__` expects 1 parameter, 2 were given
|
87 | ...
88 |
89 | def __next__(self, x): # #16217 assert special method is linted, expects 1 parameter
| ^^^^^^^^ PLE0302
90 | ...
|
unexpected_special_method_signature.py:92:9: PLE0302 The special method `__buffer__` expects 2 parameters, 1 was given
|
90 | ...
91 |
92 | def __buffer__(self): # #16217 assert special method is linted, expects 2 parameters
| ^^^^^^^^^^ PLE0302
93 | ...
|
unexpected_special_method_signature.py:95:9: PLE0302 The special method `__class_getitem__` expects 2 parameters, 1 was given
|
93 | ...
94 |
95 | def __class_getitem__(self): # #16217 assert special method is linted, expects 2 parameters
| ^^^^^^^^^^^^^^^^^ PLE0302
96 | ...
|
unexpected_special_method_signature.py:98:9: PLE0302 The special method `__mro_entries__` expects 2 parameters, 1 was given
|
96 | ...
97 |
98 | def __mro_entries__(self): # #16217 assert special method is linted, expects 2 parameters
| ^^^^^^^^^^^^^^^ PLE0302
99 | ...
|
unexpected_special_method_signature.py:101:9: PLE0302 The special method `__release_buffer__` expects 2 parameters, 1 was given
|
99 | ...
100 |
101 | def __release_buffer__(self): # #16217 assert special method is linted, expects 2 parameters
| ^^^^^^^^^^^^^^^^^^ PLE0302
102 | ...
|
unexpected_special_method_signature.py:104:9: PLE0302 The special method `__subclasshook__` expects 2 parameters, 1 was given
|
102 | ...
103 |
104 | def __subclasshook__(self): # #16217 assert special method is linted, expects 2 parameters
| ^^^^^^^^^^^^^^^^ PLE0302
105 | ...
|
unexpected_special_method_signature.py:107:9: PLE0302 The special method `__setattr__` expects 3 parameters, 2 were given
|
105 | ...
106 |
107 | def __setattr__(self, /, name): # #16217 assert positional-only special method is linted, expects 3 parameters
| ^^^^^^^^^^^ PLE0302
108 | ...
|
unexpected_special_method_signature.py:110:9: PLE0302 The special method `__setitem__` expects 3 parameters, 4 were given
|
108 | ...
109 |
110 | def __setitem__(self, key, /, value, extra_value): # #16217 assert positional-only special method is linted, expects 3 parameters
| ^^^^^^^^^^^ PLE0302
111 | ...
|

View File

@@ -165,7 +165,7 @@ pub(super) fn dataclass_kind<'a>(
/// Returns `true` if the given class has "default copy" semantics.
///
/// For example, Pydantic `BaseModel` and `BaseSettings` subclassses copy attribute defaults on
/// For example, Pydantic `BaseModel` and `BaseSettings` subclasses copy attribute defaults on
/// instance creation. As such, the use of mutable default values is safe for such classes.
pub(super) fn has_default_copy_semantics(
class_def: &ast::StmtClassDef,
@@ -174,7 +174,16 @@ pub(super) fn has_default_copy_semantics(
analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
matches!(
qualified_name.segments(),
["pydantic", "BaseModel" | "BaseSettings" | "BaseConfig"]
[
"pydantic",
"BaseModel" | "RootModel" | "BaseSettings" | "BaseConfig"
] | ["pydantic", "generics", "GenericModel"]
| [
"pydantic",
"v1",
"BaseModel" | "BaseSettings" | "BaseConfig"
]
| ["pydantic", "v1", "generics", "GenericModel"]
| ["pydantic_settings", "BaseSettings"]
| ["msgspec", "Struct"]
| ["sqlmodel", "SQLModel"]

View File

@@ -31,78 +31,78 @@ RUF012.py:25:26: RUF012 Mutable class attributes should be annotated with `typin
27 | class_variable: ClassVar[list[int]] = []
|
RUF012.py:89:38: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
87 | class I(SQLModel):
88 | id: int
89 | mutable_default: list[int] = []
| ^^ RUF012
90 |
91 | from sqlmodel import SQLModel
|
RUF012.py:114:36: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
RUF012.py:103:38: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
112 | }
113 |
114 | mutable_default: 'list[int]' = []
101 | class K(SQLModel):
102 | id: int
103 | mutable_default: list[int] = []
| ^^ RUF012
104 |
105 | from sqlmodel import SQLModel
|
RUF012.py:128:36: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
126 | }
127 |
128 | mutable_default: 'list[int]' = []
| ^^ RUF012
115 | immutable_annotation: 'Sequence[int]'= []
116 | without_annotation = []
129 | immutable_annotation: 'Sequence[int]'= []
130 | without_annotation = []
|
RUF012.py:115:44: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
RUF012.py:129:44: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
114 | mutable_default: 'list[int]' = []
115 | immutable_annotation: 'Sequence[int]'= []
128 | mutable_default: 'list[int]' = []
129 | immutable_annotation: 'Sequence[int]'= []
| ^^ RUF012
116 | without_annotation = []
117 | class_variable: 'ClassVar[list[int]]' = []
130 | without_annotation = []
131 | class_variable: 'ClassVar[list[int]]' = []
|
RUF012.py:116:26: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
RUF012.py:130:26: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
114 | mutable_default: 'list[int]' = []
115 | immutable_annotation: 'Sequence[int]'= []
116 | without_annotation = []
128 | mutable_default: 'list[int]' = []
129 | immutable_annotation: 'Sequence[int]'= []
130 | without_annotation = []
| ^^ RUF012
117 | class_variable: 'ClassVar[list[int]]' = []
118 | final_variable: 'Final[list[int]]' = []
131 | class_variable: 'ClassVar[list[int]]' = []
132 | final_variable: 'Final[list[int]]' = []
|
RUF012.py:117:45: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
RUF012.py:131:45: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
115 | immutable_annotation: 'Sequence[int]'= []
116 | without_annotation = []
117 | class_variable: 'ClassVar[list[int]]' = []
129 | immutable_annotation: 'Sequence[int]'= []
130 | without_annotation = []
131 | class_variable: 'ClassVar[list[int]]' = []
| ^^ RUF012
118 | final_variable: 'Final[list[int]]' = []
119 | class_variable_without_subscript: 'ClassVar' = []
132 | final_variable: 'Final[list[int]]' = []
133 | class_variable_without_subscript: 'ClassVar' = []
|
RUF012.py:118:42: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
RUF012.py:132:42: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
116 | without_annotation = []
117 | class_variable: 'ClassVar[list[int]]' = []
118 | final_variable: 'Final[list[int]]' = []
130 | without_annotation = []
131 | class_variable: 'ClassVar[list[int]]' = []
132 | final_variable: 'Final[list[int]]' = []
| ^^ RUF012
119 | class_variable_without_subscript: 'ClassVar' = []
120 | final_variable_without_subscript: 'Final' = []
133 | class_variable_without_subscript: 'ClassVar' = []
134 | final_variable_without_subscript: 'Final' = []
|
RUF012.py:119:52: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
RUF012.py:133:52: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
117 | class_variable: 'ClassVar[list[int]]' = []
118 | final_variable: 'Final[list[int]]' = []
119 | class_variable_without_subscript: 'ClassVar' = []
131 | class_variable: 'ClassVar[list[int]]' = []
132 | final_variable: 'Final[list[int]]' = []
133 | class_variable_without_subscript: 'ClassVar' = []
| ^^ RUF012
120 | final_variable_without_subscript: 'Final' = []
134 | final_variable_without_subscript: 'Final' = []
|
RUF012.py:120:49: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
RUF012.py:134:49: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
118 | final_variable: 'Final[list[int]]' = []
119 | class_variable_without_subscript: 'ClassVar' = []
120 | final_variable_without_subscript: 'Final' = []
132 | final_variable: 'Final[list[int]]' = []
133 | class_variable_without_subscript: 'ClassVar' = []
134 | final_variable_without_subscript: 'Final' = []
| ^^ RUF012
|

View File

@@ -24,19 +24,22 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream> {
}) => {
let mut output = vec![];
let rename_value =
RenameValue::from_attributes(struct_attributes.as_slice()).unwrap_or_default();
for field in &fields.named {
if let Some(attr) = field
.attrs
.iter()
.find(|attr| attr.path().is_ident("option"))
{
output.push(handle_option(field, attr)?);
output.push(handle_option(field, attr, rename_value)?);
} else if field
.attrs
.iter()
.any(|attr| attr.path().is_ident("option_group"))
{
output.push(handle_option_group(field)?);
output.push(handle_option_group(field, rename_value)?);
} else if let Some(serde) = field
.attrs
.iter()
@@ -86,8 +89,8 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream> {
Ok(quote! {
#[automatically_derived]
impl crate::options_base::OptionsMetadata for #ident {
fn record(visit: &mut dyn crate::options_base::Visit) {
impl ruff_workspace::options_base::OptionsMetadata for #ident {
fn record(visit: &mut dyn ruff_workspace::options_base::Visit) {
#(#output);*
}
@@ -105,7 +108,10 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream> {
/// For a field with type `Option<Foobar>` where `Foobar` itself is a struct
/// deriving `ConfigurationOptions`, create code that calls retrieves options
/// from that group: `Foobar::get_available_options()`
fn handle_option_group(field: &Field) -> syn::Result<proc_macro2::TokenStream> {
fn handle_option_group(
field: &Field,
rename_value: RenameValue,
) -> syn::Result<proc_macro2::TokenStream> {
let ident = field
.ident
.as_ref()
@@ -122,10 +128,10 @@ fn handle_option_group(field: &Field) -> syn::Result<proc_macro2::TokenStream> {
PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }),
}) if type_ident == "Option" => {
let path = &args[0];
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
let renamed_field = rename_value.apply(ident);
Ok(quote_spanned!(
ident.span() => (visit.record_set(#kebab_name, crate::options_base::OptionSet::of::<#path>()))
ident.span() => (visit.record_set(#renamed_field, ruff_workspace::options_base::OptionSet::of::<#path>()))
))
}
_ => Err(syn::Error::new(
@@ -154,7 +160,11 @@ fn parse_doc(doc: &Attribute) -> syn::Result<String> {
/// Parse an `#[option(doc="...", default="...", value_type="...",
/// example="...")]` attribute and return data in the form of an `OptionField`.
fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::TokenStream> {
fn handle_option(
field: &Field,
attr: &Attribute,
rename_value: RenameValue,
) -> syn::Result<proc_macro2::TokenStream> {
let docs: Vec<&Attribute> = field
.attrs
.iter()
@@ -190,8 +200,7 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::To
example,
scope,
} = parse_field_attributes(attr)?;
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
let renamed_field = rename_value.apply(ident);
let scope = if let Some(scope) = scope {
quote!(Some(#scope))
} else {
@@ -214,14 +223,14 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::To
let note = quote_option(deprecated.note);
let since = quote_option(deprecated.since);
quote!(Some(crate::options_base::Deprecated { since: #since, message: #note }))
quote!(Some(ruff_workspace::options_base::Deprecated { since: #since, message: #note }))
} else {
quote!(None)
};
Ok(quote_spanned!(
ident.span() => {
visit.record_field(#kebab_name, crate::options_base::OptionField{
visit.record_field(#renamed_field, ruff_workspace::options_base::OptionField{
doc: &#doc,
default: &#default,
value_type: &#value_type,
@@ -351,3 +360,66 @@ struct DeprecatedAttribute {
since: Option<String>,
note: Option<String>,
}
#[derive(Copy, Clone, Default)]
enum RenameValue {
#[default]
KebabCase,
CamelCase,
}
impl RenameValue {
fn from_attributes(attrs: &[Attribute]) -> Option<RenameValue> {
let serde = attrs.iter().find(|attr| attr.path().is_ident("serde"))?;
let Meta::List(list) = &serde.meta else {
return None;
};
let mut rename_value = None;
let _ = list.parse_nested_meta(|meta| {
if meta.path.is_ident("rename_all") {
let value = meta.value()?;
let s: LitStr = value.parse()?;
match s.value().as_str() {
"kebab-case" => {
rename_value = Some(RenameValue::KebabCase);
Ok(())
}
"camelCase" => {
rename_value = Some(RenameValue::CamelCase);
Ok(())
}
_ => Err(meta.error("Expected `kebab-case` or `camelCase`")),
}
} else {
Err(meta.error("Expected `rename_all`"))
}
});
rename_value
}
fn apply(self, ident: &syn::Ident) -> syn::LitStr {
let renamed = match self {
RenameValue::KebabCase => ident.to_string().replace('_', "-"),
RenameValue::CamelCase => {
let mut result = String::new();
let mut capitalize = false;
for c in ident.to_string().chars() {
if c == '_' {
capitalize = true;
} else if capitalize {
result.push(c.to_ascii_uppercase());
capitalize = false;
} else {
result.push(c);
}
}
result
}
};
LitStr::new(&renamed, ident.span())
}
}

View File

@@ -16,6 +16,7 @@ license = { workspace = true }
ruff_diagnostics = { workspace = true }
ruff_formatter = { workspace = true }
ruff_linter = { workspace = true }
ruff_macros = { workspace = true }
ruff_notebook = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_codegen = { workspace = true }

View File

@@ -2,8 +2,9 @@
pub use edit::{DocumentKey, NotebookDocument, PositionEncoding, TextDocument};
use lsp_types::CodeActionKind;
pub use server::{Server, Workspace, Workspaces};
pub use server::Server;
pub use session::{ClientSettings, DocumentQuery, DocumentSnapshot, Session};
pub use workspace::{Workspace, Workspaces};
#[macro_use]
mod message;
@@ -16,6 +17,7 @@ mod logging;
mod resolve;
mod server;
mod session;
mod workspace;
pub(crate) const SERVER_NAME: &str = "ruff";
pub(crate) const DIAGNOSTIC_NAME: &str = "Ruff";

View File

@@ -3,14 +3,11 @@
use lsp_server as lsp;
use lsp_types as types;
use lsp_types::InitializeParams;
use lsp_types::WorkspaceFolder;
use std::num::NonZeroUsize;
use std::ops::Deref;
// The new PanicInfoHook name requires MSRV >= 1.82
#[allow(deprecated)]
use std::panic::PanicInfo;
use std::str::FromStr;
use thiserror::Error;
use types::ClientCapabilities;
use types::CodeActionKind;
use types::CodeActionOptions;
@@ -24,7 +21,6 @@ use types::OneOf;
use types::TextDocumentSyncCapability;
use types::TextDocumentSyncKind;
use types::TextDocumentSyncOptions;
use types::Url;
use types::WorkDoneProgressOptions;
use types::WorkspaceFoldersServerCapabilities;
@@ -34,9 +30,8 @@ use self::schedule::event_loop_thread;
use self::schedule::Scheduler;
use self::schedule::Task;
use crate::session::AllSettings;
use crate::session::ClientSettings;
use crate::session::Session;
use crate::session::WorkspaceSettingsMap;
use crate::workspace::Workspaces;
use crate::PositionEncoding;
mod api;
@@ -447,122 +442,3 @@ impl FromStr for SupportedCommand {
})
}
}
#[derive(Debug)]
pub struct Workspaces(Vec<Workspace>);
impl Workspaces {
pub fn new(workspaces: Vec<Workspace>) -> Self {
Self(workspaces)
}
/// Create the workspaces from the provided workspace folders as provided by the client during
/// initialization.
fn from_workspace_folders(
workspace_folders: Option<Vec<WorkspaceFolder>>,
mut workspace_settings: WorkspaceSettingsMap,
) -> std::result::Result<Workspaces, WorkspacesError> {
let mut client_settings_for_url = |url: &Url| {
workspace_settings.remove(url).unwrap_or_else(|| {
tracing::info!(
"No workspace settings found for {}, using default settings",
url
);
ClientSettings::default()
})
};
let workspaces =
if let Some(folders) = workspace_folders.filter(|folders| !folders.is_empty()) {
folders
.into_iter()
.map(|folder| {
let settings = client_settings_for_url(&folder.uri);
Workspace::new(folder.uri).with_settings(settings)
})
.collect()
} else {
let current_dir = std::env::current_dir().map_err(WorkspacesError::Io)?;
tracing::info!(
"No workspace(s) were provided during initialization. \
Using the current working directory as a default workspace: {}",
current_dir.display()
);
let uri = Url::from_file_path(current_dir)
.map_err(|()| WorkspacesError::InvalidCurrentDir)?;
let settings = client_settings_for_url(&uri);
vec![Workspace::default(uri).with_settings(settings)]
};
Ok(Workspaces(workspaces))
}
}
impl Deref for Workspaces {
type Target = [Workspace];
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Error, Debug)]
enum WorkspacesError {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("Failed to create a URL from the current working directory")]
InvalidCurrentDir,
}
#[derive(Debug)]
pub struct Workspace {
/// The [`Url`] pointing to the root of the workspace.
url: Url,
/// The client settings for this workspace.
settings: Option<ClientSettings>,
/// Whether this is the default workspace as created by the server. This will be the case when
/// no workspace folders were provided during initialization.
is_default: bool,
}
impl Workspace {
/// Create a new workspace with the given root URL.
pub fn new(url: Url) -> Self {
Self {
url,
settings: None,
is_default: false,
}
}
/// Create a new default workspace with the given root URL.
pub fn default(url: Url) -> Self {
Self {
url,
settings: None,
is_default: true,
}
}
/// Set the client settings for this workspace.
#[must_use]
pub fn with_settings(mut self, settings: ClientSettings) -> Self {
self.settings = Some(settings);
self
}
/// Returns the root URL of the workspace.
pub(crate) fn url(&self) -> &Url {
&self.url
}
/// Returns the client settings for this workspace.
pub(crate) fn settings(&self) -> Option<&ClientSettings> {
self.settings.as_ref()
}
/// Returns true if this is the default workspace.
pub(crate) fn is_default(&self) -> bool {
self.is_default
}
}

View File

@@ -7,7 +7,7 @@ use lsp_types::{ClientCapabilities, FileEvent, NotebookDocumentCellChange, Url};
use settings::ResolvedClientSettings;
use crate::edit::{DocumentKey, DocumentVersion, NotebookDocument};
use crate::server::Workspaces;
use crate::workspace::Workspaces;
use crate::{PositionEncoding, TextDocument};
pub(crate) use self::capabilities::ResolvedClientCapabilities;

View File

@@ -11,7 +11,7 @@ use thiserror::Error;
pub(crate) use ruff_settings::RuffSettings;
use crate::edit::LanguageId;
use crate::server::{Workspace, Workspaces};
use crate::workspace::{Workspace, Workspaces};
use crate::{
edit::{DocumentKey, DocumentVersion, NotebookDocument},
PositionEncoding, TextDocument,

View File

@@ -5,6 +5,8 @@ use rustc_hash::FxHashMap;
use serde::Deserialize;
use ruff_linter::{line_width::LineLength, RuleSelector};
use ruff_macros::OptionsMetadata;
use ruff_workspace::options_base::{OptionField, OptionsMetadata};
/// Maps a workspace URI to its associated client settings. Used during server initialization.
pub(crate) type WorkspaceSettingsMap = FxHashMap<Url, ClientSettings>;
@@ -57,27 +59,76 @@ pub(crate) enum ConfigurationPreference {
EditorOnly,
}
/// This is a direct representation of the settings schema sent by the client.
#[derive(Debug, Deserialize, Default)]
#[derive(Debug, Deserialize, Default, OptionsMetadata)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
pub struct ClientSettings {
/// Path to a `ruff.toml` or `pyproject.toml` file to use for configuration.
///
/// By default, Ruff will discover configuration for each project from the filesystem,
/// mirroring the behavior of the Ruff CLI.
#[option(
default = r#"null"#,
value_type = "string",
example = r#""~/path/to/ruff.toml""#
)]
configuration: Option<String>,
fix_all: Option<bool>,
organize_imports: Option<bool>,
lint: Option<LintOptions>,
format: Option<FormatOptions>,
code_action: Option<CodeActionOptions>,
exclude: Option<Vec<String>>,
line_length: Option<LineLength>,
/// The strategy to use when resolving settings across VS Code and the filesystem. By default,
/// editor configuration is prioritized over `ruff.toml` and `pyproject.toml` files.
///
/// * `"editorFirst"`: Editor settings take priority over configuration files present in the
/// workspace.
/// * `"filesystemFirst"`: Configuration files present in the workspace takes priority over
/// editor settings.
/// * `"editorOnly"`: Ignore configuration files entirely i.e., only use editor settings.
#[option(
default = r#""editorFirst""#,
value_type = r#""editorFirst" | "filesystemFirst" | "editorOnly""#,
example = r#""filesystemFirst""#
)]
configuration_preference: Option<ConfigurationPreference>,
/// If `true` or [`None`], show syntax errors as diagnostics.
/// A list of file patterns to exclude from linting and formatting. See [the
/// documentation](https://docs.astral.sh/ruff/settings/#exclude) for more details.
#[option(
default = r#"null"#,
value_type = "string[]",
example = r#"["**/tests/**"]"#
)]
exclude: Option<Vec<String>>,
/// The line length to use for the linter and formatter.
#[option(default = "null", value_type = "int", example = "100")]
line_length: Option<LineLength>,
/// Whether to register the server as capable of handling `source.fixAll` code actions.
#[option(default = "true", value_type = "bool", example = "false")]
fix_all: Option<bool>,
/// Whether to register the server as capable of handling `source.organizeImports` code
/// actions.
#[option(default = "true", value_type = "bool", example = "false")]
organize_imports: Option<bool>,
/// _New in Ruff [v0.5.0](https://astral.sh/blog/ruff-v0.5.0#changes-to-e999-and-reporting-of-syntax-errors)_
///
/// Whether to show syntax error diagnostics.
///
/// This is useful when using Ruff with other language servers, allowing the user to refer
/// to syntax errors from only one source.
#[option(default = "true", value_type = "bool", example = "false")]
show_syntax_errors: Option<bool>,
#[option_group]
lint: Option<LintOptions>,
#[option_group]
format: Option<FormatOptions>,
#[option_group]
code_action: Option<CodeActionOptions>,
// These settings are only needed for tracing, and are only read from the global configuration.
// These will not be in the resolved settings.
#[serde(flatten)]
@@ -98,14 +149,26 @@ impl ClientSettings {
}
}
/// Settings needed to initialize tracing. These will only be
/// read from the global configuration.
#[derive(Debug, Deserialize, Default)]
#[derive(Debug, Deserialize, Default, OptionsMetadata)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
pub(crate) struct TracingSettings {
/// The log level to use for the server.
#[option(
default = r#""info""#,
value_type = r#""error" | "warn" | "info" | "debug" | "trace""#,
example = r#""debug""#
)]
pub(crate) log_level: Option<crate::logging::LogLevel>,
/// Path to the log file - tildes and environment variables are supported.
/// Path to the log file to use for the server.
///
/// If not set, logs will be written to stderr. Tildes and environment variables are expanded.
#[option(
default = r#"null"#,
value_type = "string",
example = r#""~/path/to/ruff.log""#
)]
pub(crate) log_file: Option<PathBuf>,
}
@@ -121,14 +184,31 @@ struct WorkspaceSettings {
workspace: Url,
}
#[derive(Debug, Default, Deserialize)]
/// Settings specific to the Ruff linter.
#[derive(Debug, Default, Deserialize, OptionsMetadata)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
struct LintOptions {
/// Whether to enable linting. Set to `false` to use Ruff exclusively as a formatter.
#[option(default = "true", value_type = "bool", example = "false")]
enable: Option<bool>,
/// Whether to enable Ruff's preview mode when linting.
#[option(default = "null", value_type = "bool", example = "true")]
preview: Option<bool>,
/// Rules to enable by default. See [the
/// documentation](https://docs.astral.sh/ruff/settings/#lint_select).
#[option(default = "null", value_type = "string[]", example = r#"["E", "F"]"#)]
select: Option<Vec<String>>,
/// Rules to enable in addition to those in [`lint.select`](#select).
#[option(default = "null", value_type = "string[]", example = r#"["W"]"#)]
extend_select: Option<Vec<String>>,
/// Rules to disable by default. See [the
/// documentation](https://docs.astral.sh/ruff/settings/#lint_ignore).
#[option(default = "null", value_type = "string[]", example = r#"["E4", "E7"]"#)]
ignore: Option<Vec<String>>,
}
@@ -143,10 +223,13 @@ impl LintOptions {
}
}
#[derive(Debug, Default, Deserialize)]
/// Settings specific to the Ruff formatter.
#[derive(Debug, Default, Deserialize, OptionsMetadata)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
struct FormatOptions {
/// Whether to enable Ruff's preview mode when formatting.
#[option(default = "null", value_type = "bool", example = "true")]
preview: Option<bool>,
}
@@ -176,6 +259,38 @@ struct CodeActionParameters {
enable: Option<bool>,
}
impl OptionsMetadata for CodeActionOptions {
fn record(visit: &mut dyn ruff_workspace::options_base::Visit) {
visit.record_field(
"disableRuleComment.enable",
OptionField {
doc: "Whether to display Quick Fix actions to disable rules via `noqa` suppression comments.",
default: "true",
value_type: "bool",
scope: None,
example: "false",
deprecated: None,
},
);
visit.record_field(
"fixViolation.enable",
OptionField {
doc: "Whether to display Quick Fix actions to autofix violations.",
default: "true",
value_type: "bool",
scope: None,
example: "false",
deprecated: None,
},
);
}
fn documentation() -> Option<&'static str> {
Some("Enable or disable code actions provided by the server.")
}
}
/// This is the exact schema for initialization options sent in by the client
/// during initialization.
#[derive(Debug, Deserialize)]

View File

@@ -0,0 +1,126 @@
use std::ops::Deref;
use lsp_types::{Url, WorkspaceFolder};
use thiserror::Error;
use crate::session::WorkspaceSettingsMap;
use crate::ClientSettings;
#[derive(Debug)]
pub struct Workspaces(Vec<Workspace>);
impl Workspaces {
pub fn new(workspaces: Vec<Workspace>) -> Self {
Self(workspaces)
}
/// Create the workspaces from the provided workspace folders as provided by the client during
/// initialization.
pub(crate) fn from_workspace_folders(
workspace_folders: Option<Vec<WorkspaceFolder>>,
mut workspace_settings: WorkspaceSettingsMap,
) -> std::result::Result<Workspaces, WorkspacesError> {
let mut client_settings_for_url = |url: &Url| {
workspace_settings.remove(url).unwrap_or_else(|| {
tracing::info!(
"No workspace settings found for {}, using default settings",
url
);
ClientSettings::default()
})
};
let workspaces =
if let Some(folders) = workspace_folders.filter(|folders| !folders.is_empty()) {
folders
.into_iter()
.map(|folder| {
let settings = client_settings_for_url(&folder.uri);
Workspace::new(folder.uri).with_settings(settings)
})
.collect()
} else {
let current_dir = std::env::current_dir().map_err(WorkspacesError::Io)?;
tracing::info!(
"No workspace(s) were provided during initialization. \
Using the current working directory as a default workspace: {}",
current_dir.display()
);
let uri = Url::from_file_path(current_dir)
.map_err(|()| WorkspacesError::InvalidCurrentDir)?;
let settings = client_settings_for_url(&uri);
vec![Workspace::default(uri).with_settings(settings)]
};
Ok(Workspaces(workspaces))
}
}
impl Deref for Workspaces {
type Target = [Workspace];
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Error, Debug)]
pub(crate) enum WorkspacesError {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("Failed to create a URL from the current working directory")]
InvalidCurrentDir,
}
#[derive(Debug)]
pub struct Workspace {
/// The [`Url`] pointing to the root of the workspace.
url: Url,
/// The client settings for this workspace.
settings: Option<ClientSettings>,
/// Whether this is the default workspace as created by the server. This will be the case when
/// no workspace folders were provided during initialization.
is_default: bool,
}
impl Workspace {
/// Create a new workspace with the given root URL.
pub fn new(url: Url) -> Self {
Self {
url,
settings: None,
is_default: false,
}
}
/// Create a new default workspace with the given root URL.
pub fn default(url: Url) -> Self {
Self {
url,
settings: None,
is_default: true,
}
}
/// Set the client settings for this workspace.
#[must_use]
pub fn with_settings(mut self, settings: ClientSettings) -> Self {
self.settings = Some(settings);
self
}
/// Returns the root URL of the workspace.
pub(crate) fn url(&self) -> &Url {
&self.url
}
/// Returns the client settings for this workspace.
pub(crate) fn settings(&self) -> Option<&ClientSettings> {
self.settings.as_ref()
}
/// Returns true if this is the default workspace.
pub(crate) fn is_default(&self) -> bool {
self.is_default
}
}

View File

@@ -6,6 +6,7 @@ use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use strum::IntoEnumIterator;
use crate as ruff_workspace;
use crate::options_base::{OptionsMetadata, Visit};
use crate::settings::LineEnding;
use ruff_formatter::IndentStyle;

View File

@@ -29,7 +29,7 @@ ruff_python_formatter = { path = "../crates/ruff_python_formatter" }
ruff_text_size = { path = "../crates/ruff_text_size" }
libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "351d9cf0037be949d17800d0c7b4838e533c2ed6" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "c8826fa4d1d9e3cba4c6e578763878b71fa9a10d" }
similar = { version = "2.5.0" }
tracing = { version = "0.1.40" }