Compare commits
25 Commits
dhruv/serv
...
david/make
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bac74a120b | ||
|
|
1ab975b142 | ||
|
|
92416e1f85 | ||
|
|
5ecea4e81f | ||
|
|
b3a0353bf2 | ||
|
|
9953dede9e | ||
|
|
fed67170ec | ||
|
|
cc5270ae9c | ||
|
|
c0fc2796a2 | ||
|
|
c5224316c0 | ||
|
|
67087c0417 | ||
|
|
72fe5525ab | ||
|
|
ff290172d7 | ||
|
|
7673b7265d | ||
|
|
caca1874ae | ||
|
|
08f4c60660 | ||
|
|
e86c21e90a | ||
|
|
c84f1e0c72 | ||
|
|
d6ae12c05f | ||
|
|
0743c21811 | ||
|
|
c322baaaef | ||
|
|
ce3dcb066c | ||
|
|
f406835639 | ||
|
|
30383d4855 | ||
|
|
c7d97c3cd5 |
148
Cargo.lock
generated
148
Cargo.lock
generated
@@ -8,6 +8,18 @@ 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"
|
||||
@@ -128,6 +140,12 @@ 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"
|
||||
@@ -209,12 +227,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "boxcar"
|
||||
version = "0.2.10"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225450ee9328e1e828319b48a89726cffc1b0ad26fd9211ad435de9fa376acae"
|
||||
dependencies = [
|
||||
"loom",
|
||||
]
|
||||
checksum = "2721c3c5a6f0e7f7e607125d963fedeb765f545f67adc9d71ed934693881eb42"
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
@@ -998,19 +1013,6 @@ 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"
|
||||
@@ -1100,6 +1102,10 @@ 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"
|
||||
@@ -1107,18 +1113,17 @@ 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.10.0"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.2",
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1174,7 +1179,7 @@ dependencies = [
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.52.0",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1678,19 +1683,6 @@ 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"
|
||||
@@ -2822,7 +2814,6 @@ dependencies = [
|
||||
"ruff_python_formatter",
|
||||
"ruff_python_parser",
|
||||
"ruff_python_trivia",
|
||||
"ruff_server",
|
||||
"ruff_workspace",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -3173,7 +3164,6 @@ dependencies = [
|
||||
"ruff_diagnostics",
|
||||
"ruff_formatter",
|
||||
"ruff_linter",
|
||||
"ruff_macros",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_codegen",
|
||||
@@ -3325,14 +3315,14 @@ checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=c8826fa4d1d9e3cba4c6e578763878b71fa9a10d#c8826fa4d1d9e3cba4c6e578763878b71fa9a10d"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
|
||||
dependencies = [
|
||||
"append-only-vec",
|
||||
"arc-swap",
|
||||
"boxcar",
|
||||
"compact_str",
|
||||
"crossbeam-queue",
|
||||
"crossbeam",
|
||||
"dashmap 6.1.0",
|
||||
"hashbrown 0.15.2",
|
||||
"hashbrown 0.14.5",
|
||||
"hashlink",
|
||||
"indexmap",
|
||||
"parking_lot",
|
||||
@@ -3347,12 +3337,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=c8826fa4d1d9e3cba4c6e578763878b71fa9a10d#c8826fa4d1d9e3cba4c6e578763878b71fa9a10d"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=c8826fa4d1d9e3cba4c6e578763878b71fa9a10d#c8826fa4d1d9e3cba4c6e578763878b71fa9a10d"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -3394,12 +3384,6 @@ 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"
|
||||
@@ -4467,16 +4451,6 @@ 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"
|
||||
@@ -4486,60 +4460,6 @@ 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"
|
||||
|
||||
@@ -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 = "c8826fa4d1d9e3cba4c6e578763878b71fa9a10d" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "351d9cf0037be949d17800d0c7b4838e533c2ed6" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
|
||||
@@ -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, ScopedConstraintId};
|
||||
use crate::semantic_index::constraint::PatternConstraintKind;
|
||||
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, ScopedEagerBindingsId, UseDefMapBuilder,
|
||||
EagerBindingsKey, FlowSnapshot, ScopedConstraintId, 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 {
|
||||
fn current_visibility_constraints_mut(&mut self) -> &mut VisibilityConstraintsBuilder<'db> {
|
||||
let scope_id = self.current_scope();
|
||||
&mut self.use_def_maps[scope_id].visibility_constraints
|
||||
}
|
||||
@@ -406,12 +406,16 @@ 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>) -> ScopedConstraintId {
|
||||
fn add_negated_constraint(
|
||||
&mut self,
|
||||
constraint: Constraint<'db>,
|
||||
) -> (Constraint<'db>, ScopedConstraintId) {
|
||||
let negated = Constraint {
|
||||
node: constraint.node,
|
||||
is_positive: false,
|
||||
};
|
||||
self.current_use_def_map_mut().add_constraint(negated)
|
||||
let id = self.current_use_def_map_mut().add_constraint(negated);
|
||||
(negated, id)
|
||||
}
|
||||
|
||||
/// Records a previously added constraint by adding it to all live bindings.
|
||||
@@ -427,7 +431,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
|
||||
}
|
||||
@@ -456,10 +460,9 @@ 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_id);
|
||||
.add_atom(constraint, 0);
|
||||
self.record_visibility_constraint_id(id);
|
||||
id
|
||||
}
|
||||
@@ -1189,14 +1192,12 @@ 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(first_constraint_id);
|
||||
.add_atom(constraint, 0);
|
||||
let later_vis_constraint_id = self
|
||||
.current_visibility_constraints_mut()
|
||||
.add_atom(later_constraint_id);
|
||||
.add_atom(constraint, 1);
|
||||
|
||||
// Save aside any break states from an outer loop
|
||||
let saved_break_states = std::mem::take(&mut self.loop_break_states);
|
||||
@@ -1777,13 +1778,13 @@ where
|
||||
// anymore.
|
||||
if index < values.len() - 1 {
|
||||
let constraint = self.build_constraint(value);
|
||||
let constraint_id = match op {
|
||||
ast::BoolOp::And => self.add_constraint(constraint),
|
||||
let (constraint, constraint_id) = match op {
|
||||
ast::BoolOp::And => (constraint, self.add_constraint(constraint)),
|
||||
ast::BoolOp::Or => self.add_negated_constraint(constraint),
|
||||
};
|
||||
let visibility_constraint = self
|
||||
.current_visibility_constraints_mut()
|
||||
.add_atom(constraint_id);
|
||||
.add_atom(constraint, 0);
|
||||
|
||||
let after_expr = self.flow_snapshot();
|
||||
|
||||
|
||||
@@ -1,40 +1,10 @@
|
||||
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>,
|
||||
|
||||
@@ -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 `constraints`
|
||||
//! [`ScopedConstraintId`], which are indices into the `all_definitions` and `all_constraints`
|
||||
//! indexvecs in the [`UseDefMap`].
|
||||
//!
|
||||
//! There is another special kind of possible "definition" for a symbol: there might be a path from
|
||||
@@ -255,27 +255,28 @@
|
||||
//! 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.
|
||||
|
||||
use ruff_index::{newtype_index, IndexVec};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
pub(crate) use self::symbol_state::ScopedConstraintId;
|
||||
use self::symbol_state::{
|
||||
ConstraintIndexIterator, LiveBindingsIterator, LiveDeclaration, LiveDeclarationsIterator,
|
||||
BindingIdWithConstraintsIterator, ConstraintIdIterator, DeclarationIdIterator,
|
||||
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::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;
|
||||
|
||||
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> {
|
||||
@@ -284,10 +285,10 @@ pub(crate) struct UseDefMap<'db> {
|
||||
all_definitions: IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
|
||||
|
||||
/// Array of [`Constraint`] in this scope.
|
||||
constraints: Constraints<'db>,
|
||||
all_constraints: AllConstraints<'db>,
|
||||
|
||||
/// Array of visibility constraints in this scope.
|
||||
visibility_constraints: VisibilityConstraints,
|
||||
visibility_constraints: VisibilityConstraints<'db>,
|
||||
|
||||
/// [`SymbolBindings`] reaching a [`ScopedUseId`].
|
||||
bindings_by_use: IndexVec<ScopedUseId, SymbolBindings>,
|
||||
@@ -369,7 +370,7 @@ impl<'db> UseDefMap<'db> {
|
||||
) -> BindingWithConstraintsIterator<'map, 'db> {
|
||||
BindingWithConstraintsIterator {
|
||||
all_definitions: &self.all_definitions,
|
||||
constraints: &self.constraints,
|
||||
all_constraints: &self.all_constraints,
|
||||
visibility_constraints: &self.visibility_constraints,
|
||||
inner: bindings.iter(),
|
||||
}
|
||||
@@ -381,7 +382,6 @@ 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>>>,
|
||||
pub(crate) constraints: &'map Constraints<'db>,
|
||||
pub(crate) visibility_constraints: &'map VisibilityConstraints,
|
||||
inner: LiveBindingsIterator<'map>,
|
||||
all_constraints: &'map AllConstraints<'db>,
|
||||
pub(crate) visibility_constraints: &'map VisibilityConstraints<'db>,
|
||||
inner: BindingIdWithConstraintsIterator<'map>,
|
||||
}
|
||||
|
||||
impl<'map, 'db> Iterator for BindingWithConstraintsIterator<'map, 'db> {
|
||||
type Item = BindingWithConstraints<'map, 'db>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let constraints = self.constraints;
|
||||
let all_constraints = self.all_constraints;
|
||||
|
||||
self.inner
|
||||
.next()
|
||||
.map(|live_binding| BindingWithConstraints {
|
||||
binding: self.all_definitions[live_binding.binding],
|
||||
narrowing_constraints: ConstraintsIterator {
|
||||
constraints,
|
||||
constraint_ids: live_binding.narrowing_constraints.iter(),
|
||||
.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,
|
||||
},
|
||||
visibility_constraint: live_binding.visibility_constraint,
|
||||
visibility_constraint: binding_id_with_constraints.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) narrowing_constraints: ConstraintsIterator<'map, 'db>,
|
||||
pub(crate) constraints: ConstraintsIterator<'map, 'db>,
|
||||
pub(crate) visibility_constraint: ScopedVisibilityConstraintId,
|
||||
}
|
||||
|
||||
pub(crate) struct ConstraintsIterator<'map, 'db> {
|
||||
constraints: &'map Constraints<'db>,
|
||||
constraint_ids: ConstraintIndexIterator<'map>,
|
||||
all_constraints: &'map AllConstraints<'db>,
|
||||
constraint_ids: ConstraintIdIterator<'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.constraints[ScopedConstraintId::from_u32(constraint_id)])
|
||||
.map(|constraint_id| self.all_constraints[constraint_id])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,9 +466,8 @@ impl std::iter::FusedIterator for ConstraintsIterator<'_, '_> {}
|
||||
|
||||
pub(crate) struct DeclarationsIterator<'map, 'db> {
|
||||
all_definitions: &'map IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
|
||||
pub(crate) constraints: &'map Constraints<'db>,
|
||||
pub(crate) visibility_constraints: &'map VisibilityConstraints,
|
||||
inner: LiveDeclarationsIterator<'map>,
|
||||
pub(crate) visibility_constraints: &'map VisibilityConstraints<'db>,
|
||||
inner: DeclarationIdIterator<'map>,
|
||||
}
|
||||
|
||||
pub(crate) struct DeclarationWithConstraint<'db> {
|
||||
@@ -481,13 +480,13 @@ impl<'db> Iterator for DeclarationsIterator<'_, 'db> {
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next().map(
|
||||
|LiveDeclaration {
|
||||
declaration,
|
||||
|DeclarationIdWithConstraint {
|
||||
definition,
|
||||
visibility_constraint,
|
||||
}| {
|
||||
DeclarationWithConstraint {
|
||||
declaration: self.all_definitions[*declaration],
|
||||
visibility_constraint: *visibility_constraint,
|
||||
declaration: self.all_definitions[definition],
|
||||
visibility_constraint,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -508,11 +507,11 @@ pub(super) struct UseDefMapBuilder<'db> {
|
||||
/// Append-only array of [`Definition`].
|
||||
all_definitions: IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
|
||||
|
||||
/// Builder of constraints.
|
||||
constraints: ConstraintsBuilder<'db>,
|
||||
/// Append-only array of [`Constraint`].
|
||||
all_constraints: AllConstraints<'db>,
|
||||
|
||||
/// Builder of visibility constraints.
|
||||
pub(super) visibility_constraints: VisibilityConstraintsBuilder,
|
||||
pub(super) visibility_constraints: VisibilityConstraintsBuilder<'db>,
|
||||
|
||||
/// 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
|
||||
@@ -541,7 +540,7 @@ impl Default for UseDefMapBuilder<'_> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
all_definitions: IndexVec::from_iter([None]),
|
||||
constraints: ConstraintsBuilder::default(),
|
||||
all_constraints: IndexVec::new(),
|
||||
visibility_constraints: VisibilityConstraintsBuilder::default(),
|
||||
scope_start_visibility: ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
bindings_by_use: IndexVec::new(),
|
||||
@@ -574,7 +573,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
}
|
||||
|
||||
pub(super) fn add_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
|
||||
self.constraints.add_constraint(constraint)
|
||||
self.all_constraints.push(constraint)
|
||||
}
|
||||
|
||||
pub(super) fn record_constraint_id(&mut self, constraint: ScopedConstraintId) {
|
||||
@@ -754,6 +753,7 @@ 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,
|
||||
constraints: self.constraints.build(),
|
||||
all_constraints: self.all_constraints,
|
||||
visibility_constraints: self.visibility_constraints.build(),
|
||||
bindings_by_use: self.bindings_by_use,
|
||||
public_symbols: self.symbol_states,
|
||||
|
||||
@@ -25,6 +25,13 @@ 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;
|
||||
@@ -86,6 +93,19 @@ 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();
|
||||
@@ -138,15 +158,6 @@ 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);
|
||||
}
|
||||
@@ -224,6 +235,59 @@ 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);
|
||||
|
||||
@@ -46,16 +46,14 @@
|
||||
|
||||
use itertools::{EitherOrBoth, Itertools};
|
||||
use ruff_index::newtype_index;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use 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 {
|
||||
@@ -67,54 +65,89 @@ 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 or declarations per symbol at a given time; more will
|
||||
/// go to heap.
|
||||
const INLINE_DEFINITIONS_PER_SYMBOL: usize = 4;
|
||||
/// 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;
|
||||
|
||||
/// Which constraints apply to a given binding?
|
||||
type Constraints = BitSet<INLINE_CONSTRAINT_BLOCKS>;
|
||||
|
||||
pub(super) type ConstraintIndexIterator<'a> = BitSetIterator<'a, 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>;
|
||||
|
||||
/// 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 {
|
||||
/// A list of live declarations for this symbol, sorted by their `ScopedDefinitionId`
|
||||
live_declarations: SmallVec<[LiveDeclaration; INLINE_DEFINITIONS_PER_SYMBOL]>,
|
||||
}
|
||||
/// [`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,
|
||||
|
||||
/// 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,
|
||||
/// For each live declaration, which visibility constraint applies to it?
|
||||
pub(crate) visibility_constraints: VisibilityConstraintPerDeclaration,
|
||||
}
|
||||
|
||||
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: smallvec![initial_declaration],
|
||||
live_declarations: Declarations::with(0),
|
||||
visibility_constraints: VisibilityConstraintPerDeclaration::from_iter([
|
||||
scope_start_visibility,
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a newly-encountered declaration for this symbol.
|
||||
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,
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
/// Add given visibility constraint to all live declarations.
|
||||
@@ -123,62 +156,45 @@ impl SymbolDeclarations {
|
||||
visibility_constraints: &mut VisibilityConstraintsBuilder,
|
||||
constraint: ScopedVisibilityConstraintId,
|
||||
) {
|
||||
for declaration in &mut self.live_declarations {
|
||||
declaration.visibility_constraint = visibility_constraints
|
||||
.add_and_constraint(declaration.visibility_constraint, constraint);
|
||||
for existing in &mut self.visibility_constraints {
|
||||
*existing = visibility_constraints.add_and_constraint(*existing, constraint);
|
||||
}
|
||||
}
|
||||
|
||||
/// Return an iterator over live declarations for this symbol.
|
||||
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;
|
||||
pub(super) fn iter(&self) -> DeclarationIdIterator {
|
||||
DeclarationIdIterator {
|
||||
declarations: self.live_declarations.iter(),
|
||||
visibility_constraints: self.visibility_constraints.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
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 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)) {
|
||||
// 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)) {
|
||||
match zipped {
|
||||
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::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::Left(declaration) | EitherOrBoth::Right(declaration) => {
|
||||
self.live_declarations.push(declaration);
|
||||
EitherOrBoth::Left((_, vis_constraint))
|
||||
| EitherOrBoth::Right((_, vis_constraint)) => {
|
||||
self.visibility_constraints.push(vis_constraint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,52 +205,57 @@ impl SymbolDeclarations {
|
||||
/// with a set of narrowing constraints and a visibility constraint.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update)]
|
||||
pub(super) struct SymbolBindings {
|
||||
/// A list of live bindings for this symbol, sorted by their `ScopedDefinitionId`
|
||||
live_bindings: SmallVec<[LiveBinding; INLINE_DEFINITIONS_PER_SYMBOL]>,
|
||||
}
|
||||
/// [`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,
|
||||
|
||||
/// 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,
|
||||
}
|
||||
/// 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,
|
||||
|
||||
pub(super) type LiveBindingsIterator<'a> = std::slice::Iter<'a, LiveBinding>;
|
||||
/// For each live binding, which visibility constraint applies to it?
|
||||
visibility_constraints: VisibilityConstraintPerBinding,
|
||||
}
|
||||
|
||||
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: smallvec![initial_binding],
|
||||
live_bindings: Bindings::with(ScopedDefinitionId::UNBOUND.as_u32()),
|
||||
constraints: ConstraintsPerBinding::from_iter([Constraints::default()]),
|
||||
visibility_constraints: VisibilityConstraintPerBinding::from_iter([
|
||||
scope_start_visibility,
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a newly-encountered binding for this symbol.
|
||||
pub(super) fn record_binding(
|
||||
&mut self,
|
||||
binding: ScopedDefinitionId,
|
||||
binding_id: ScopedDefinitionId,
|
||||
visibility_constraint: ScopedVisibilityConstraintId,
|
||||
) {
|
||||
// The new binding replaces all previous live bindings in this path, and has no
|
||||
// constraints.
|
||||
self.live_bindings.clear();
|
||||
self.live_bindings.push(LiveBinding {
|
||||
binding,
|
||||
narrowing_constraints: Constraints::default(),
|
||||
visibility_constraint,
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
/// Add given constraint to all live bindings.
|
||||
pub(super) fn record_constraint(&mut self, constraint_id: ScopedConstraintId) {
|
||||
for binding in &mut self.live_bindings {
|
||||
binding.narrowing_constraints.insert(constraint_id.into());
|
||||
for bitset in &mut self.constraints {
|
||||
bitset.insert(constraint_id.into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,67 +265,71 @@ impl SymbolBindings {
|
||||
visibility_constraints: &mut VisibilityConstraintsBuilder,
|
||||
constraint: ScopedVisibilityConstraintId,
|
||||
) {
|
||||
for binding in &mut self.live_bindings {
|
||||
binding.visibility_constraint = visibility_constraints
|
||||
.add_and_constraint(binding.visibility_constraint, constraint);
|
||||
for existing in &mut self.visibility_constraints {
|
||||
*existing = visibility_constraints.add_and_constraint(*existing, constraint);
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over currently live bindings for this symbol
|
||||
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;
|
||||
pub(super) fn iter(&self) -> BindingIdWithConstraintsIterator {
|
||||
BindingIdWithConstraintsIterator {
|
||||
definitions: self.live_bindings.iter(),
|
||||
constraints: self.constraints.iter(),
|
||||
visibility_constraints: self.visibility_constraints.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, b: Self, visibility_constraints: &mut VisibilityConstraintsBuilder) {
|
||||
let a = std::mem::take(self);
|
||||
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);
|
||||
|
||||
// Invariant: merge_join_by consumes the two iterators in sorted order, which ensures that
|
||||
// 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)) {
|
||||
// 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)) {
|
||||
match zipped {
|
||||
EitherOrBoth::Both(a, b) => {
|
||||
EitherOrBoth::Both(
|
||||
((_, a_constraints), a_vis_constraint),
|
||||
((_, b_constraints), b_vis_constraint),
|
||||
) => {
|
||||
// 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 mut narrowing_constraints = a.narrowing_constraints;
|
||||
narrowing_constraints.intersect(&b.narrowing_constraints);
|
||||
let constraints = a_constraints;
|
||||
constraints.intersect(b_constraints);
|
||||
self.constraints.push(std::mem::take(constraints));
|
||||
|
||||
// For visibility constraints, we merge them using a ternary OR operation:
|
||||
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,
|
||||
});
|
||||
let vis_constraint = visibility_constraints
|
||||
.add_or_constraint(a_vis_constraint, b_vis_constraint);
|
||||
self.visibility_constraints.push(vis_constraint);
|
||||
}
|
||||
|
||||
EitherOrBoth::Left(binding) | EitherOrBoth::Right(binding) => {
|
||||
self.live_bindings.push(binding);
|
||||
EitherOrBoth::Left(((_, constraints), vis_constraint))
|
||||
| EitherOrBoth::Right(((_, constraints), vis_constraint)) => {
|
||||
self.constraints.push(std::mem::take(constraints));
|
||||
self.visibility_constraints.push(vis_constraint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -354,14 +379,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) {
|
||||
self.bindings
|
||||
.simplify_visibility_constraints(snapshot_state.bindings);
|
||||
self.declarations
|
||||
.simplify_visibility_constraints(snapshot_state.declarations);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a newly-encountered declaration of this symbol.
|
||||
@@ -389,6 +414,98 @@ 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::*;
|
||||
@@ -398,16 +515,16 @@ mod tests {
|
||||
let actual = symbol
|
||||
.bindings()
|
||||
.iter()
|
||||
.map(|live_binding| {
|
||||
let def_id = live_binding.binding;
|
||||
.map(|def_id_with_constraints| {
|
||||
let def_id = def_id_with_constraints.definition;
|
||||
let def = if def_id == ScopedDefinitionId::UNBOUND {
|
||||
"unbound".into()
|
||||
} else {
|
||||
def_id.as_u32().to_string()
|
||||
};
|
||||
let constraints = live_binding
|
||||
.narrowing_constraints
|
||||
.iter()
|
||||
let constraints = def_id_with_constraints
|
||||
.constraint_ids
|
||||
.map(ScopedConstraintId::as_u32)
|
||||
.map(|idx| idx.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
@@ -423,14 +540,14 @@ mod tests {
|
||||
.declarations()
|
||||
.iter()
|
||||
.map(
|
||||
|LiveDeclaration {
|
||||
declaration,
|
||||
|DeclarationIdWithConstraint {
|
||||
definition,
|
||||
visibility_constraint: _,
|
||||
}| {
|
||||
if *declaration == ScopedDefinitionId::UNBOUND {
|
||||
if definition == ScopedDefinitionId::UNBOUND {
|
||||
"undeclared".into()
|
||||
} else {
|
||||
declaration.as_u32().to_string()
|
||||
definition.as_u32().to_string()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
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::{global_scope, use_def_map, DeclarationWithConstraint};
|
||||
use crate::semantic_index::{self, global_scope, use_def_map, DeclarationWithConstraint};
|
||||
use crate::semantic_index::{
|
||||
symbol_table, BindingWithConstraints, BindingWithConstraintsIterator, DeclarationsIterator,
|
||||
};
|
||||
@@ -13,8 +14,6 @@ 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,
|
||||
@@ -184,34 +183,20 @@ 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 an explicit module-global symbol as seen from within the same file.
|
||||
/// Infers the public type of a module-global symbol as seen from within the same file.
|
||||
///
|
||||
/// 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.
|
||||
/// If it's not defined explicitly in the global scope, it will look it up in `types.ModuleType`
|
||||
/// with a few very special exceptions.
|
||||
///
|
||||
/// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports).
|
||||
pub(crate) fn explicit_global_symbol<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
|
||||
pub(crate) fn global_symbol<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
|
||||
symbol_impl(
|
||||
db,
|
||||
global_scope(db, file),
|
||||
name,
|
||||
RequiresExplicitReExport::No,
|
||||
)
|
||||
}
|
||||
|
||||
/// 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))
|
||||
.or_fall_back_to(db, || module_type_symbol(db, name))
|
||||
}
|
||||
|
||||
/// Infers the public type of an imported symbol.
|
||||
@@ -219,16 +204,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 `module_type_implicit_global_symbol`,
|
||||
// but there are two crucial differences here:
|
||||
// We do a more limited version of this in `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 `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
|
||||
// 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
|
||||
// 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, || {
|
||||
@@ -254,7 +239,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_implicit_global_symbol(db, symbol)
|
||||
module_type_symbol(db, symbol)
|
||||
})
|
||||
})
|
||||
.unwrap_or(Symbol::Unbound)
|
||||
@@ -503,7 +488,6 @@ 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();
|
||||
|
||||
@@ -515,9 +499,9 @@ fn symbol_from_bindings_impl<'db>(
|
||||
Some(BindingWithConstraints {
|
||||
binding,
|
||||
visibility_constraint,
|
||||
narrowing_constraints: _,
|
||||
constraints: _,
|
||||
}) if binding.map_or(true, is_non_exported) => {
|
||||
visibility_constraints.evaluate(db, constraints, *visibility_constraint)
|
||||
visibility_constraints.evaluate(db, *visibility_constraint)
|
||||
}
|
||||
_ => Truthiness::AlwaysFalse,
|
||||
};
|
||||
@@ -525,7 +509,7 @@ fn symbol_from_bindings_impl<'db>(
|
||||
let mut types = bindings_with_constraints.filter_map(
|
||||
|BindingWithConstraints {
|
||||
binding,
|
||||
narrowing_constraints,
|
||||
constraints,
|
||||
visibility_constraint,
|
||||
}| {
|
||||
let binding = binding?;
|
||||
@@ -534,14 +518,13 @@ fn symbol_from_bindings_impl<'db>(
|
||||
return None;
|
||||
}
|
||||
|
||||
let static_visibility =
|
||||
visibility_constraints.evaluate(db, constraints, visibility_constraint);
|
||||
let static_visibility = visibility_constraints.evaluate(db, visibility_constraint);
|
||||
|
||||
if static_visibility.is_always_false() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut constraint_tys = narrowing_constraints
|
||||
let mut constraint_tys = constraints
|
||||
.filter_map(|constraint| narrowing_constraint(db, constraint, binding))
|
||||
.peekable();
|
||||
|
||||
@@ -592,7 +575,6 @@ 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();
|
||||
|
||||
@@ -605,7 +587,7 @@ fn symbol_from_declarations_impl<'db>(
|
||||
declaration,
|
||||
visibility_constraint,
|
||||
}) if declaration.map_or(true, is_non_exported) => {
|
||||
visibility_constraints.evaluate(db, constraints, *visibility_constraint)
|
||||
visibility_constraints.evaluate(db, *visibility_constraint)
|
||||
}
|
||||
_ => Truthiness::AlwaysFalse,
|
||||
};
|
||||
@@ -621,8 +603,7 @@ fn symbol_from_declarations_impl<'db>(
|
||||
return None;
|
||||
}
|
||||
|
||||
let static_visibility =
|
||||
visibility_constraints.evaluate(db, constraints, visibility_constraint);
|
||||
let static_visibility = visibility_constraints.evaluate(db, visibility_constraint);
|
||||
|
||||
if static_visibility.is_always_false() {
|
||||
None
|
||||
@@ -677,106 +658,63 @@ fn symbol_from_declarations_impl<'db>(
|
||||
}
|
||||
}
|
||||
|
||||
mod implicit_globals {
|
||||
use ruff_python_ast as ast;
|
||||
/// 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();
|
||||
};
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::semantic_index::{self, symbol_table};
|
||||
use crate::types::KnownClass;
|
||||
let module_type_scope = module_type.body_scope(db);
|
||||
let module_type_symbol_table = symbol_table(db, module_type_scope);
|
||||
|
||||
use super::Symbol;
|
||||
// `__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()
|
||||
}
|
||||
|
||||
/// 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));
|
||||
}
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -890,36 +828,15 @@ 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 implicit_builtin_globals() {
|
||||
fn module_type_symbols_includes_declared_types_but_not_referenced_types() {
|
||||
let db = setup_db();
|
||||
assert_bound_string_symbol(&db, builtins_symbol(&db, "__name__"));
|
||||
}
|
||||
let symbol_names = module_type_symbols(&db);
|
||||
|
||||
#[test]
|
||||
fn implicit_typing_globals() {
|
||||
let db = setup_db();
|
||||
assert_bound_string_symbol(&db, typing_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_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__"));
|
||||
let property_symbol_name = ast::name::Name::new_static("property");
|
||||
assert!(!symbol_names.contains(&property_symbol_name));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ use crate::semantic_index::{
|
||||
};
|
||||
use crate::suppression::check_suppressions;
|
||||
use crate::symbol::{
|
||||
imported_symbol, known_module_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
|
||||
Boundness, LookupError, LookupResult, Symbol, SymbolAndQualifiers,
|
||||
global_symbol, 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,7 +1586,10 @@ impl<'db> Type<'db> {
|
||||
.ignore_possibly_unbound()?
|
||||
.try_call(
|
||||
db,
|
||||
&CallArguments::positional([instance.unwrap_or(Type::none(db)), owner]),
|
||||
CallArguments::positional(
|
||||
db,
|
||||
[instance.unwrap_or(Type::none(db)), owner],
|
||||
),
|
||||
)
|
||||
.map(|outcome| Some(outcome.return_type(db)))
|
||||
.unwrap_or(None)
|
||||
@@ -1702,9 +1705,7 @@ impl<'db> Type<'db> {
|
||||
Type::AlwaysTruthy => Truthiness::AlwaysTrue,
|
||||
Type::AlwaysFalsy => Truthiness::AlwaysFalse,
|
||||
Type::Instance(InstanceType { class }) => {
|
||||
if class.is_known(db, KnownClass::Bool) {
|
||||
Truthiness::Ambiguous
|
||||
} else if class.is_known(db, KnownClass::NoneType) {
|
||||
if class.is_known(db, KnownClass::NoneType) {
|
||||
Truthiness::AlwaysFalse
|
||||
} else {
|
||||
// We only check the `__bool__` method for truth testing, even though at
|
||||
@@ -1712,7 +1713,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())
|
||||
.try_call_dunder(db, "__bool__", CallArguments::none(db))
|
||||
.map(|outcome| outcome.return_type(db))
|
||||
{
|
||||
bool_val.into()
|
||||
@@ -1785,7 +1786,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()) {
|
||||
let return_ty = match self.try_call_dunder(db, "__len__", CallArguments::none(db)) {
|
||||
Ok(outcome) | Err(CallDunderError::PossiblyUnbound(outcome)) => outcome.return_type(db),
|
||||
|
||||
// TODO: emit a diagnostic
|
||||
@@ -1801,359 +1802,377 @@ impl<'db> Type<'db> {
|
||||
fn try_call(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
arguments: CallArguments<'db>,
|
||||
) -> Result<CallOutcome<'db>, CallError<'db>> {
|
||||
match self {
|
||||
Type::Callable(CallableType::BoundMethod(bound_method)) => {
|
||||
let instance = bound_method.self_instance(db);
|
||||
let arguments = arguments.with_self(instance);
|
||||
#[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);
|
||||
|
||||
let binding = bind_call(
|
||||
db,
|
||||
&arguments,
|
||||
bound_method.function(db).signature(db),
|
||||
self,
|
||||
);
|
||||
let binding = bind_call(
|
||||
db,
|
||||
arguments,
|
||||
bound_method.function(db).signature(db),
|
||||
ty_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().is_some_and(|ty| ty.is_none(db));
|
||||
let first_argument_is_none = arguments
|
||||
.first_argument(db)
|
||||
.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 {
|
||||
let signature = Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::new(
|
||||
Some("owner".into()),
|
||||
Some(KnownClass::Type.to_instance(db)),
|
||||
Some("instance".into()),
|
||||
Some(Type::object(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, 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().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,
|
||||
)))
|
||||
}
|
||||
),
|
||||
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, self);
|
||||
let binding = bind_call(db, arguments, &signature, ty_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), 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(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 binding = bind_call(db, arguments, &signature, ty_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), ty_self);
|
||||
|
||||
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)));
|
||||
if binding.has_binding_errors() {
|
||||
return Err(CallError::BindingError { binding });
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
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::Repr) => {
|
||||
if let Some(first_arg) = binding.one_parameter_type() {
|
||||
binding.set_return_type(first_arg.repr(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::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::Repr) => {
|
||||
if let Some(first_arg) = binding.one_parameter_type() {
|
||||
binding.set_return_type(first_arg.repr(db));
|
||||
};
|
||||
}
|
||||
|
||||
Some(KnownFunction::GetattrStatic) => {
|
||||
let Some((instance_ty, attr_name, default)) =
|
||||
binding.three_parameter_types()
|
||||
else {
|
||||
return Ok(CallOutcome::Single(binding));
|
||||
};
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let Some(attr_name) = attr_name.into_string_literal() 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 default = if default.is_unknown() {
|
||||
Type::Never
|
||||
} else {
|
||||
default
|
||||
};
|
||||
let Some(attr_name) = attr_name.into_string_literal() else {
|
||||
return Ok(CallOutcome::Single(binding));
|
||||
};
|
||||
|
||||
let union_with_default = |ty| UnionType::from_elements(db, [ty, default]);
|
||||
let default = if default.is_unknown() {
|
||||
Type::Never
|
||||
} else {
|
||||
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.
|
||||
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.
|
||||
|
||||
union_with_default(ty)
|
||||
}
|
||||
}
|
||||
Symbol::Type(ty, Boundness::PossiblyUnbound) => {
|
||||
union_with_default(ty)
|
||||
}
|
||||
}
|
||||
Symbol::Type(ty, Boundness::PossiblyUnbound) => {
|
||||
union_with_default(ty)
|
||||
}
|
||||
Symbol::Unbound => default,
|
||||
},
|
||||
);
|
||||
Symbol::Unbound => default,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
};
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// 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 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()
|
||||
.map(|arg| arg.str(db))
|
||||
.unwrap_or(Type::string_literal(db, "")),
|
||||
// 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: self,
|
||||
}
|
||||
}
|
||||
CallDunderError::Call(CallError::Union {
|
||||
called_ty: _,
|
||||
bindings,
|
||||
errors,
|
||||
}) => CallError::Union {
|
||||
called_ty: self,
|
||||
bindings,
|
||||
errors,
|
||||
_ => Type::Instance(InstanceType { class }),
|
||||
},
|
||||
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),
|
||||
)))
|
||||
}
|
||||
|
||||
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::MethodNotAvailable => {
|
||||
// Turn "`X.__call__` unbound" into "`X` not callable"
|
||||
CallError::NotCallable {
|
||||
not_callable_ty: self,
|
||||
CallDunderError::Call(CallError::Union {
|
||||
called_ty: _,
|
||||
bindings,
|
||||
errors,
|
||||
}) => CallError::Union {
|
||||
called_ty: 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: ty_self,
|
||||
outcome: Box::new(outcome),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
CallDunderError::MethodNotAvailable => {
|
||||
// Turn "`X.__call__` unbound" into "`X` not callable"
|
||||
CallError::NotCallable {
|
||||
not_callable_ty: ty_self,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 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,
|
||||
}),
|
||||
}
|
||||
|
||||
// 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,
|
||||
}),
|
||||
}
|
||||
|
||||
try_call_query(db, self, arguments)
|
||||
}
|
||||
|
||||
/// Return the outcome of calling an class/instance attribute of this type
|
||||
@@ -2166,13 +2185,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(*receiver_ty))
|
||||
self.try_call(db, arguments.with_self(db, *receiver_ty))
|
||||
}
|
||||
|
||||
Type::Instance(_) | Type::ClassLiteral(_) => self.try_call(db, arguments),
|
||||
@@ -2199,7 +2218,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) => {
|
||||
@@ -2228,12 +2247,12 @@ impl<'db> Type<'db> {
|
||||
};
|
||||
}
|
||||
|
||||
let dunder_iter_result = self.try_call_dunder(db, "__iter__", &CallArguments::none());
|
||||
let dunder_iter_result = self.try_call_dunder(db, "__iter__", CallArguments::none(db));
|
||||
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()) {
|
||||
return match iterator_ty.try_call_dunder(db, "__next__", CallArguments::none(db)) {
|
||||
Ok(outcome) => {
|
||||
if matches!(
|
||||
dunder_iter_result,
|
||||
@@ -2281,7 +2300,7 @@ impl<'db> Type<'db> {
|
||||
match self.try_call_dunder(
|
||||
db,
|
||||
"__getitem__",
|
||||
&CallArguments::positional([KnownClass::Int.to_instance(db)]),
|
||||
CallArguments::positional(db, [KnownClass::Int.to_instance(db)]),
|
||||
) {
|
||||
Ok(outcome) => IterationOutcome::Iterable {
|
||||
element_ty: outcome.return_type(db),
|
||||
@@ -4150,9 +4169,9 @@ impl<'db> Class<'db> {
|
||||
let namespace = KnownClass::Dict.to_instance(db);
|
||||
|
||||
// TODO: Other keyword arguments?
|
||||
let arguments = CallArguments::positional([name, bases, namespace]);
|
||||
let arguments = CallArguments::positional(db, [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 {
|
||||
@@ -5084,7 +5103,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::{global_symbol, typing_extensions_symbol, typing_symbol};
|
||||
use crate::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;
|
||||
|
||||
@@ -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)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
|
||||
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)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
|
||||
pub(super) enum CallError<'db> {
|
||||
/// The type is not callable.
|
||||
NotCallable {
|
||||
|
||||
@@ -1,63 +1,60 @@
|
||||
use ruff_python_ast::name::Name;
|
||||
|
||||
use crate::Db;
|
||||
|
||||
use super::Type;
|
||||
|
||||
/// Typed arguments for a single call, in source order.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct CallArguments<'a, 'db>(Vec<Argument<'a, 'db>>);
|
||||
#[salsa::interned]
|
||||
pub(crate) struct CallArguments<'db> {
|
||||
args: Vec<Argument<'db>>,
|
||||
}
|
||||
|
||||
impl<'a, 'db> CallArguments<'a, 'db> {
|
||||
impl<'a, 'db> CallArguments<'db> {
|
||||
/// Create a [`CallArguments`] with no arguments.
|
||||
pub(crate) fn none() -> Self {
|
||||
Self(Vec::new())
|
||||
pub(crate) fn none(db: &'db dyn Db) -> Self {
|
||||
CallArguments::new(db, Vec::new())
|
||||
}
|
||||
|
||||
/// Create a [`CallArguments`] from an iterator over non-variadic positional argument types.
|
||||
pub(crate) fn positional(positional_tys: impl IntoIterator<Item = Type<'db>>) -> Self {
|
||||
positional_tys
|
||||
.into_iter()
|
||||
.map(Argument::Positional)
|
||||
.collect()
|
||||
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<_>>(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Prepend an extra positional argument.
|
||||
pub(crate) fn with_self(&self, self_ty: Type<'db>) -> Self {
|
||||
let mut arguments = Vec::with_capacity(self.0.len() + 1);
|
||||
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);
|
||||
arguments.push(Argument::Synthetic(self_ty));
|
||||
arguments.extend_from_slice(&self.0);
|
||||
Self(arguments)
|
||||
arguments.extend_from_slice(&self.args(db));
|
||||
CallArguments::new(db, arguments)
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = &Argument<'a, 'db>> {
|
||||
self.0.iter()
|
||||
pub(crate) fn args_iter(&self, db: &'db dyn Db) -> impl IntoIterator<Item = Argument<'db>> {
|
||||
self.args(db).into_iter()
|
||||
}
|
||||
|
||||
// TODO this should be eliminated in favor of [`bind_call`]
|
||||
pub(crate) fn first_argument(&self) -> Option<Type<'db>> {
|
||||
self.0.first().map(Argument::ty)
|
||||
pub(crate) fn first_argument(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
self.args(db).first().map(Argument::ty)
|
||||
}
|
||||
|
||||
// TODO this should be eliminated in favor of [`bind_call`]
|
||||
pub(crate) fn second_argument(&self) -> Option<Type<'db>> {
|
||||
self.0.get(1).map(Argument::ty)
|
||||
pub(crate) fn second_argument(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
self.args(db).get(1).map(Argument::ty)
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
|
||||
pub(crate) enum Argument<'db> {
|
||||
/// The synthetic `self` or `cls` argument, which doesn't appear explicitly at the call site.
|
||||
Synthetic(Type<'db>),
|
||||
/// A positional argument.
|
||||
@@ -65,12 +62,12 @@ pub(crate) enum Argument<'a, 'db> {
|
||||
/// A starred positional argument (e.g. `*args`).
|
||||
Variadic(Type<'db>),
|
||||
/// A keyword argument (e.g. `a=1`).
|
||||
Keyword { name: &'a str, ty: Type<'db> },
|
||||
Keyword { name: Name, 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,
|
||||
|
||||
@@ -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.iter().enumerate() {
|
||||
for (argument_index, argument) in arguments.args_iter(db).into_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)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
|
||||
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)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
|
||||
pub(crate) enum CallBindingError<'db> {
|
||||
/// The type of an argument is not assignable to the annotated type of its corresponding
|
||||
/// parameter.
|
||||
|
||||
@@ -50,8 +50,7 @@ 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, explicit_global_symbol,
|
||||
module_type_implicit_global_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
|
||||
builtins_module_scope, builtins_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
|
||||
typing_extensions_symbol, LookupError,
|
||||
};
|
||||
use crate::types::call::{Argument, CallArguments};
|
||||
@@ -91,7 +90,7 @@ use super::slots::check_class_slots;
|
||||
use super::string_annotation::{
|
||||
parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION,
|
||||
};
|
||||
use super::{CallDunderError, ParameterExpectation, ParameterExpectations};
|
||||
use super::{global_symbol, 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
|
||||
@@ -1647,7 +1646,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
|
||||
let target_ty = enter_ty
|
||||
.try_call(self.db(), &CallArguments::positional([context_expression_ty]))
|
||||
.try_call(self.db(), CallArguments::positional(self.db(), [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.
|
||||
@@ -1692,12 +1691,15 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
if exit_ty
|
||||
.try_call(
|
||||
self.db(),
|
||||
&CallArguments::positional([
|
||||
context_manager_ty,
|
||||
Type::none(self.db()),
|
||||
Type::none(self.db()),
|
||||
Type::none(self.db()),
|
||||
]),
|
||||
CallArguments::positional(
|
||||
self.db(),
|
||||
[
|
||||
context_manager_ty,
|
||||
Type::none(self.db()),
|
||||
Type::none(self.db()),
|
||||
Type::none(self.db()),
|
||||
],
|
||||
),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
@@ -2242,7 +2244,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
{
|
||||
let call = class_member.try_call(
|
||||
self.db(),
|
||||
&CallArguments::positional([target_type, value_type]),
|
||||
CallArguments::positional(self.db(), [target_type, value_type]),
|
||||
);
|
||||
let augmented_return_ty = match call {
|
||||
Ok(t) => t.return_type(self.db()),
|
||||
@@ -2723,46 +2725,53 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
&mut self,
|
||||
arguments: &'a ast::Arguments,
|
||||
parameter_expectations: ParameterExpectations,
|
||||
) -> 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,
|
||||
};
|
||||
) -> 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,
|
||||
};
|
||||
|
||||
match arg_or_keyword {
|
||||
ast::ArgOrKeyword::Arg(arg) => match arg {
|
||||
ast::Expr::Starred(ast::ExprStarred {
|
||||
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,
|
||||
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,
|
||||
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)
|
||||
if let Some(arg) = arg {
|
||||
Argument::Keyword {
|
||||
name: arg.id.clone(),
|
||||
ty,
|
||||
}
|
||||
} else {
|
||||
// TODO diagnostic if not last
|
||||
Argument::Keywords(ty)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
|
||||
fn infer_optional_expression(&mut self, expression: Option<&ast::Expr>) -> Option<Type<'db>> {
|
||||
@@ -3278,7 +3287,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) => {
|
||||
@@ -3572,7 +3581,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
|
||||
Symbol::Unbound
|
||||
// No nonlocal binding? Check the module's explicit globals.
|
||||
// No nonlocal binding? Check the module's 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() {
|
||||
@@ -3589,12 +3598,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
explicit_global_symbol(db, self.file(), symbol_name)
|
||||
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, || {
|
||||
@@ -3786,7 +3791,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
match operand_type.try_call_dunder(
|
||||
self.db(),
|
||||
unary_dunder_method,
|
||||
&CallArguments::none(),
|
||||
CallArguments::none(self.db()),
|
||||
) {
|
||||
Ok(outcome) => outcome.return_type(self.db()),
|
||||
Err(e) => {
|
||||
@@ -4037,7 +4042,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.try_call_dunder(
|
||||
self.db(),
|
||||
reflected_dunder,
|
||||
&CallArguments::positional([left_ty]),
|
||||
CallArguments::positional(self.db(), [left_ty]),
|
||||
)
|
||||
.map(|outcome| outcome.return_type(self.db()))
|
||||
.or_else(|_| {
|
||||
@@ -4045,7 +4050,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.try_call_dunder(
|
||||
self.db(),
|
||||
op.dunder(),
|
||||
&CallArguments::positional([right_ty]),
|
||||
CallArguments::positional(self.db(), [right_ty]),
|
||||
)
|
||||
.map(|outcome| outcome.return_type(self.db()))
|
||||
})
|
||||
@@ -4058,7 +4063,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
left_class.member(self.db(), op.dunder())
|
||||
{
|
||||
class_member
|
||||
.try_call(self.db(), &CallArguments::positional([left_ty, right_ty]))
|
||||
.try_call(
|
||||
self.db(),
|
||||
CallArguments::positional(self.db(), [left_ty, right_ty]),
|
||||
)
|
||||
.map(|outcome| outcome.return_type(self.db()))
|
||||
.ok()
|
||||
} else {
|
||||
@@ -4076,7 +4084,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
class_member
|
||||
.try_call(
|
||||
self.db(),
|
||||
&CallArguments::positional([right_ty, left_ty]),
|
||||
CallArguments::positional(self.db(), [right_ty, left_ty]),
|
||||
)
|
||||
.map(|outcome| outcome.return_type(self.db()))
|
||||
.ok()
|
||||
@@ -4645,21 +4653,23 @@ 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([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(
|
||||
db,
|
||||
[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) {
|
||||
@@ -4703,7 +4713,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
contains_dunder
|
||||
.try_call(
|
||||
db,
|
||||
&CallArguments::positional([Type::Instance(right), Type::Instance(left)]),
|
||||
CallArguments::positional(
|
||||
db,
|
||||
[Type::Instance(right), Type::Instance(left)],
|
||||
),
|
||||
)
|
||||
.map(|outcome| outcome.return_type(db))
|
||||
.ok()
|
||||
@@ -4961,7 +4974,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
match value_ty.try_call_dunder(
|
||||
self.db(),
|
||||
"__getitem__",
|
||||
&CallArguments::positional([slice_ty]),
|
||||
CallArguments::positional(self.db(), [slice_ty]),
|
||||
) {
|
||||
Ok(outcome) => return outcome.return_type(self.db()),
|
||||
Err(err @ CallDunderError::PossiblyUnbound { .. }) => {
|
||||
@@ -5022,7 +5035,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
|
||||
return ty
|
||||
.try_call(self.db(), &CallArguments::positional([value_ty, slice_ty]))
|
||||
.try_call(self.db(), CallArguments::positional(self.db(),[value_ty, slice_ty]))
|
||||
.map(|outcome| outcome.return_type(self.db()))
|
||||
.unwrap_or_else(|err| {
|
||||
self.context.report_lint(
|
||||
@@ -6253,7 +6266,6 @@ 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;
|
||||
|
||||
@@ -345,8 +345,7 @@ pub(crate) enum ParameterKind<'db> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::db::tests::{setup_db, TestDb};
|
||||
use crate::symbol::global_symbol;
|
||||
use crate::types::{FunctionType, KnownClass};
|
||||
use crate::types::{global_symbol, FunctionType, KnownClass};
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
|
||||
#[track_caller]
|
||||
|
||||
@@ -178,9 +178,7 @@ use std::cmp::Ordering;
|
||||
use ruff_index::{Idx, IndexVec};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::semantic_index::constraint::{
|
||||
Constraint, ConstraintNode, Constraints, PatternConstraintKind, ScopedConstraintId,
|
||||
};
|
||||
use crate::semantic_index::constraint::{Constraint, ConstraintNode, PatternConstraintKind};
|
||||
use crate::types::{infer_expression_type, Truthiness};
|
||||
use crate::Db;
|
||||
|
||||
@@ -233,15 +231,69 @@ impl std::fmt::Debug for ScopedVisibilityConstraintId {
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
struct InteriorNode {
|
||||
/// 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,
|
||||
atom: Atom,
|
||||
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(©).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 =
|
||||
@@ -284,13 +336,16 @@ 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 {
|
||||
pub(crate) struct VisibilityConstraints<'db> {
|
||||
constraints: IndexVec<Atom, Constraint<'db>>,
|
||||
interiors: IndexVec<ScopedVisibilityConstraintId, InteriorNode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) struct VisibilityConstraintsBuilder {
|
||||
pub(crate) struct VisibilityConstraintsBuilder<'db> {
|
||||
constraints: IndexVec<Atom, Constraint<'db>>,
|
||||
interiors: IndexVec<ScopedVisibilityConstraintId, InteriorNode>,
|
||||
constraint_cache: FxHashMap<Constraint<'db>, Atom>,
|
||||
interior_cache: FxHashMap<InteriorNode, ScopedVisibilityConstraintId>,
|
||||
not_cache: FxHashMap<ScopedVisibilityConstraintId, ScopedVisibilityConstraintId>,
|
||||
and_cache: FxHashMap<
|
||||
@@ -303,9 +358,10 @@ pub(crate) struct VisibilityConstraintsBuilder {
|
||||
>,
|
||||
}
|
||||
|
||||
impl VisibilityConstraintsBuilder {
|
||||
pub(crate) fn build(self) -> VisibilityConstraints {
|
||||
impl<'db> VisibilityConstraintsBuilder<'db> {
|
||||
pub(crate) fn build(self) -> VisibilityConstraints<'db> {
|
||||
VisibilityConstraints {
|
||||
constraints: self.constraints,
|
||||
interiors: self.interiors,
|
||||
}
|
||||
}
|
||||
@@ -329,6 +385,14 @@ impl VisibilityConstraintsBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -344,23 +408,17 @@ impl VisibilityConstraintsBuilder {
|
||||
.or_insert_with(|| self.interiors.push(node))
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// 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.
|
||||
pub(crate) fn add_atom(
|
||||
&mut self,
|
||||
constraint: ScopedConstraintId,
|
||||
constraint: Constraint<'db>,
|
||||
copy: u8,
|
||||
) -> ScopedVisibilityConstraintId {
|
||||
let atom = self.add_constraint(constraint, copy);
|
||||
self.add_interior(InteriorNode {
|
||||
atom: constraint,
|
||||
atom,
|
||||
if_true: ALWAYS_TRUE,
|
||||
if_ambiguous: AMBIGUOUS,
|
||||
if_false: ALWAYS_FALSE,
|
||||
@@ -530,12 +588,11 @@ impl VisibilityConstraintsBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
impl VisibilityConstraints {
|
||||
impl<'db> VisibilityConstraints<'db> {
|
||||
/// Analyze the statically known visibility for a given visibility constraint.
|
||||
pub(crate) fn evaluate<'db>(
|
||||
pub(crate) fn evaluate(
|
||||
&self,
|
||||
db: &'db dyn Db,
|
||||
constraints: &Constraints<'db>,
|
||||
mut id: ScopedVisibilityConstraintId,
|
||||
) -> Truthiness {
|
||||
loop {
|
||||
@@ -545,7 +602,7 @@ impl VisibilityConstraints {
|
||||
ALWAYS_FALSE => return Truthiness::AlwaysFalse,
|
||||
_ => self.interiors[id],
|
||||
};
|
||||
let constraint = &constraints[node.atom];
|
||||
let constraint = &self.constraints[node.atom];
|
||||
match Self::analyze_single(db, constraint) {
|
||||
Truthiness::AlwaysTrue => id = node.if_true,
|
||||
Truthiness::Ambiguous => id = node.if_ambiguous,
|
||||
|
||||
@@ -22,7 +22,6 @@ 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 }
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
//!
|
||||
//! Used for <https://docs.astral.sh/ruff/settings/>.
|
||||
use itertools::Itertools;
|
||||
use ruff_server::ClientSettings;
|
||||
use std::fmt::Write;
|
||||
|
||||
use ruff_python_trivia::textwrap;
|
||||
@@ -16,32 +15,12 @@ pub(crate) fn generate() -> String {
|
||||
&mut output,
|
||||
Set::Toplevel(Options::metadata()),
|
||||
&mut Vec::new(),
|
||||
SetKind::Ruff,
|
||||
);
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
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) {
|
||||
fn generate_set(output: &mut String, set: Set, parents: &mut Vec<Set>) {
|
||||
match &set {
|
||||
Set::Toplevel(_) => {
|
||||
output.push_str("### Top-level\n");
|
||||
@@ -74,7 +53,7 @@ fn generate_set(output: &mut String, set: Set, parents: &mut Vec<Set>, set_kind:
|
||||
|
||||
// Generate the fields.
|
||||
for (name, field) in &fields {
|
||||
emit_field(output, name, field, parents.as_slice(), set_kind);
|
||||
emit_field(output, name, field, parents.as_slice());
|
||||
output.push_str("---\n\n");
|
||||
}
|
||||
|
||||
@@ -87,7 +66,6 @@ fn generate_set(output: &mut String, set: Set, parents: &mut Vec<Set>, set_kind:
|
||||
set: *sub_set,
|
||||
},
|
||||
parents,
|
||||
set_kind,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,13 +93,7 @@ impl Set {
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_field(
|
||||
output: &mut String,
|
||||
name: &str,
|
||||
field: &OptionField,
|
||||
parents: &[Set],
|
||||
set_kind: SetKind,
|
||||
) {
|
||||
fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[Set]) {
|
||||
let header_level = if parents.is_empty() { "####" } else { "#####" };
|
||||
let parents_anchor = parents.iter().filter_map(|parent| parent.name()).join("_");
|
||||
|
||||
@@ -165,43 +137,25 @@ fn emit_field(
|
||||
output.push_str(&format!("**Type**: `{}`\n", field.value_type));
|
||||
output.push('\n');
|
||||
output.push_str("**Example usage**:\n\n");
|
||||
|
||||
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_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,
|
||||
));
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
fn format_tab(tab_name: &str, content: &str) -> String {
|
||||
fn format_tab(tab_name: &str, header: &str, content: &str) -> String {
|
||||
format!(
|
||||
"=== \"{}\"\n\n{}\n\n",
|
||||
"=== \"{}\"\n\n ```toml\n {}\n{}\n ```\n",
|
||||
tab_name,
|
||||
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, " ")
|
||||
textwrap::indent(content, " ")
|
||||
)
|
||||
}
|
||||
|
||||
@@ -233,15 +187,6 @@ 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)>,
|
||||
|
||||
@@ -46,8 +46,6 @@ 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.
|
||||
@@ -91,9 +89,6 @@ 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)?,
|
||||
|
||||
@@ -68,44 +68,8 @@ class TestClass:
|
||||
def __eq__(self, **kwargs): # ignore **kwargs
|
||||
...
|
||||
|
||||
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 __eq__(self, *, other=42): # ignore positional-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
|
||||
...
|
||||
@@ -81,42 +81,28 @@ 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 K(SQLModel):
|
||||
class I(SQLModel):
|
||||
id: int
|
||||
mutable_default: list[int] = []
|
||||
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
class L(SQLModel):
|
||||
class J(SQLModel):
|
||||
id: int
|
||||
name: str
|
||||
|
||||
|
||||
class M(SQLModel):
|
||||
class K(SQLModel):
|
||||
id: int
|
||||
i_s: list[J] = []
|
||||
|
||||
|
||||
class N(SQLModel):
|
||||
class L(SQLModel):
|
||||
id: int
|
||||
i_j: list[L] = list()
|
||||
i_j: list[K] = list()
|
||||
|
||||
# Lint should account for deferred annotations
|
||||
# See https://github.com/astral-sh/ruff/issues/15857
|
||||
|
||||
@@ -23,8 +23,10 @@ impl ExpectedParams {
|
||||
| "__neg__" | "__pos__" | "__abs__" | "__invert__" | "__complex__" | "__int__"
|
||||
| "__float__" | "__index__" | "__trunc__" | "__floor__" | "__ceil__" | "__enter__"
|
||||
| "__aenter__" | "__getnewargs_ex__" | "__getnewargs__" | "__getstate__"
|
||||
| "__reduce__" | "__copy__" | "__await__" | "__aiter__" | "__anext__"
|
||||
| "__fspath__" | "__subclasses__" | "__next__" => Some(ExpectedParams::Fixed(0)),
|
||||
| "__reduce__" | "__copy__" | "__unicode__" | "__nonzero__" | "__await__"
|
||||
| "__aiter__" | "__anext__" | "__fspath__" | "__subclasses__" => {
|
||||
Some(ExpectedParams::Fixed(0))
|
||||
}
|
||||
"__format__" | "__lt__" | "__le__" | "__eq__" | "__ne__" | "__gt__" | "__ge__"
|
||||
| "__getattr__" | "__getattribute__" | "__delattr__" | "__delete__"
|
||||
| "__instancecheck__" | "__subclasscheck__" | "__getitem__" | "__missing__"
|
||||
@@ -35,9 +37,8 @@ impl ExpectedParams {
|
||||
| "__rpow__" | "__rlshift__" | "__rrshift__" | "__rand__" | "__rxor__" | "__ror__"
|
||||
| "__iadd__" | "__isub__" | "__imul__" | "__itruediv__" | "__ifloordiv__"
|
||||
| "__imod__" | "__ilshift__" | "__irshift__" | "__iand__" | "__ixor__" | "__ior__"
|
||||
| "__ipow__" | "__setstate__" | "__reduce_ex__" | "__deepcopy__" | "__matmul__"
|
||||
| "__rmatmul__" | "__imatmul__" | "__buffer__" | "__class_getitem__"
|
||||
| "__mro_entries__" | "__release_buffer__" | "__subclasshook__" => {
|
||||
| "__ipow__" | "__setstate__" | "__reduce_ex__" | "__deepcopy__" | "__cmp__"
|
||||
| "__matmul__" | "__rmatmul__" | "__imatmul__" | "__div__" => {
|
||||
Some(ExpectedParams::Fixed(1))
|
||||
}
|
||||
"__setattr__" | "__get__" | "__set__" | "__setitem__" | "__set_name__" => {
|
||||
@@ -146,8 +147,11 @@ pub(crate) fn unexpected_special_method_signature(
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore methods with keyword-only parameters or variadic parameters.
|
||||
if !parameters.kwonlyargs.is_empty() || parameters.kwarg.is_some() {
|
||||
// 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()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -156,11 +160,10 @@ pub(crate) fn unexpected_special_method_signature(
|
||||
return;
|
||||
}
|
||||
|
||||
let actual_params = parameters.args.len() + parameters.posonlyargs.len();
|
||||
let actual_params = parameters.args.len();
|
||||
let mandatory_params = parameters
|
||||
.args
|
||||
.iter()
|
||||
.chain(parameters.posonlyargs.iter())
|
||||
.filter(|arg| arg.default.is_none())
|
||||
.count();
|
||||
|
||||
|
||||
@@ -71,75 +71,3 @@ 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 | ...
|
||||
|
|
||||
|
||||
@@ -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` subclasses copy attribute defaults on
|
||||
/// For example, Pydantic `BaseModel` and `BaseSettings` subclassses 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,16 +174,7 @@ pub(super) fn has_default_copy_semantics(
|
||||
analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
[
|
||||
"pydantic",
|
||||
"BaseModel" | "RootModel" | "BaseSettings" | "BaseConfig"
|
||||
] | ["pydantic", "generics", "GenericModel"]
|
||||
| [
|
||||
"pydantic",
|
||||
"v1",
|
||||
"BaseModel" | "BaseSettings" | "BaseConfig"
|
||||
]
|
||||
| ["pydantic", "v1", "generics", "GenericModel"]
|
||||
["pydantic", "BaseModel" | "BaseSettings" | "BaseConfig"]
|
||||
| ["pydantic_settings", "BaseSettings"]
|
||||
| ["msgspec", "Struct"]
|
||||
| ["sqlmodel", "SQLModel"]
|
||||
|
||||
@@ -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:103:38: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
|
|
||||
101 | class K(SQLModel):
|
||||
102 | id: int
|
||||
103 | mutable_default: list[int] = []
|
||||
| ^^ RUF012
|
||||
104 |
|
||||
105 | from sqlmodel import SQLModel
|
||||
|
|
||||
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:128:36: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
RUF012.py:114:36: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
|
|
||||
126 | }
|
||||
127 |
|
||||
128 | mutable_default: 'list[int]' = []
|
||||
112 | }
|
||||
113 |
|
||||
114 | mutable_default: 'list[int]' = []
|
||||
| ^^ RUF012
|
||||
129 | immutable_annotation: 'Sequence[int]'= []
|
||||
130 | without_annotation = []
|
||||
115 | immutable_annotation: 'Sequence[int]'= []
|
||||
116 | without_annotation = []
|
||||
|
|
||||
|
||||
RUF012.py:129:44: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
RUF012.py:115:44: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
|
|
||||
128 | mutable_default: 'list[int]' = []
|
||||
129 | immutable_annotation: 'Sequence[int]'= []
|
||||
114 | mutable_default: 'list[int]' = []
|
||||
115 | immutable_annotation: 'Sequence[int]'= []
|
||||
| ^^ RUF012
|
||||
130 | without_annotation = []
|
||||
131 | class_variable: 'ClassVar[list[int]]' = []
|
||||
116 | without_annotation = []
|
||||
117 | class_variable: 'ClassVar[list[int]]' = []
|
||||
|
|
||||
|
||||
RUF012.py:130:26: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
RUF012.py:116:26: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
|
|
||||
128 | mutable_default: 'list[int]' = []
|
||||
129 | immutable_annotation: 'Sequence[int]'= []
|
||||
130 | without_annotation = []
|
||||
114 | mutable_default: 'list[int]' = []
|
||||
115 | immutable_annotation: 'Sequence[int]'= []
|
||||
116 | without_annotation = []
|
||||
| ^^ RUF012
|
||||
131 | class_variable: 'ClassVar[list[int]]' = []
|
||||
132 | final_variable: 'Final[list[int]]' = []
|
||||
117 | class_variable: 'ClassVar[list[int]]' = []
|
||||
118 | final_variable: 'Final[list[int]]' = []
|
||||
|
|
||||
|
||||
RUF012.py:131:45: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
RUF012.py:117:45: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
|
|
||||
129 | immutable_annotation: 'Sequence[int]'= []
|
||||
130 | without_annotation = []
|
||||
131 | class_variable: 'ClassVar[list[int]]' = []
|
||||
115 | immutable_annotation: 'Sequence[int]'= []
|
||||
116 | without_annotation = []
|
||||
117 | class_variable: 'ClassVar[list[int]]' = []
|
||||
| ^^ RUF012
|
||||
132 | final_variable: 'Final[list[int]]' = []
|
||||
133 | class_variable_without_subscript: 'ClassVar' = []
|
||||
118 | final_variable: 'Final[list[int]]' = []
|
||||
119 | class_variable_without_subscript: 'ClassVar' = []
|
||||
|
|
||||
|
||||
RUF012.py:132:42: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
RUF012.py:118:42: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
|
|
||||
130 | without_annotation = []
|
||||
131 | class_variable: 'ClassVar[list[int]]' = []
|
||||
132 | final_variable: 'Final[list[int]]' = []
|
||||
116 | without_annotation = []
|
||||
117 | class_variable: 'ClassVar[list[int]]' = []
|
||||
118 | final_variable: 'Final[list[int]]' = []
|
||||
| ^^ RUF012
|
||||
133 | class_variable_without_subscript: 'ClassVar' = []
|
||||
134 | final_variable_without_subscript: 'Final' = []
|
||||
119 | class_variable_without_subscript: 'ClassVar' = []
|
||||
120 | final_variable_without_subscript: 'Final' = []
|
||||
|
|
||||
|
||||
RUF012.py:133:52: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
RUF012.py:119:52: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
|
|
||||
131 | class_variable: 'ClassVar[list[int]]' = []
|
||||
132 | final_variable: 'Final[list[int]]' = []
|
||||
133 | class_variable_without_subscript: 'ClassVar' = []
|
||||
117 | class_variable: 'ClassVar[list[int]]' = []
|
||||
118 | final_variable: 'Final[list[int]]' = []
|
||||
119 | class_variable_without_subscript: 'ClassVar' = []
|
||||
| ^^ RUF012
|
||||
134 | final_variable_without_subscript: 'Final' = []
|
||||
120 | final_variable_without_subscript: 'Final' = []
|
||||
|
|
||||
|
||||
RUF012.py:134:49: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
RUF012.py:120:49: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
|
|
||||
132 | final_variable: 'Final[list[int]]' = []
|
||||
133 | class_variable_without_subscript: 'ClassVar' = []
|
||||
134 | final_variable_without_subscript: 'Final' = []
|
||||
118 | final_variable: 'Final[list[int]]' = []
|
||||
119 | class_variable_without_subscript: 'ClassVar' = []
|
||||
120 | final_variable_without_subscript: 'Final' = []
|
||||
| ^^ RUF012
|
||||
|
|
||||
|
||||
@@ -24,22 +24,19 @@ 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, rename_value)?);
|
||||
output.push(handle_option(field, attr)?);
|
||||
} else if field
|
||||
.attrs
|
||||
.iter()
|
||||
.any(|attr| attr.path().is_ident("option_group"))
|
||||
{
|
||||
output.push(handle_option_group(field, rename_value)?);
|
||||
output.push(handle_option_group(field)?);
|
||||
} else if let Some(serde) = field
|
||||
.attrs
|
||||
.iter()
|
||||
@@ -89,8 +86,8 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream> {
|
||||
|
||||
Ok(quote! {
|
||||
#[automatically_derived]
|
||||
impl ruff_workspace::options_base::OptionsMetadata for #ident {
|
||||
fn record(visit: &mut dyn ruff_workspace::options_base::Visit) {
|
||||
impl crate::options_base::OptionsMetadata for #ident {
|
||||
fn record(visit: &mut dyn crate::options_base::Visit) {
|
||||
#(#output);*
|
||||
}
|
||||
|
||||
@@ -108,10 +105,7 @@ 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,
|
||||
rename_value: RenameValue,
|
||||
) -> syn::Result<proc_macro2::TokenStream> {
|
||||
fn handle_option_group(field: &Field) -> syn::Result<proc_macro2::TokenStream> {
|
||||
let ident = field
|
||||
.ident
|
||||
.as_ref()
|
||||
@@ -128,10 +122,10 @@ fn handle_option_group(
|
||||
PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }),
|
||||
}) if type_ident == "Option" => {
|
||||
let path = &args[0];
|
||||
let renamed_field = rename_value.apply(ident);
|
||||
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
|
||||
|
||||
Ok(quote_spanned!(
|
||||
ident.span() => (visit.record_set(#renamed_field, ruff_workspace::options_base::OptionSet::of::<#path>()))
|
||||
ident.span() => (visit.record_set(#kebab_name, crate::options_base::OptionSet::of::<#path>()))
|
||||
))
|
||||
}
|
||||
_ => Err(syn::Error::new(
|
||||
@@ -160,11 +154,7 @@ 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,
|
||||
rename_value: RenameValue,
|
||||
) -> syn::Result<proc_macro2::TokenStream> {
|
||||
fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::TokenStream> {
|
||||
let docs: Vec<&Attribute> = field
|
||||
.attrs
|
||||
.iter()
|
||||
@@ -200,7 +190,8 @@ fn handle_option(
|
||||
example,
|
||||
scope,
|
||||
} = parse_field_attributes(attr)?;
|
||||
let renamed_field = rename_value.apply(ident);
|
||||
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
|
||||
|
||||
let scope = if let Some(scope) = scope {
|
||||
quote!(Some(#scope))
|
||||
} else {
|
||||
@@ -223,14 +214,14 @@ fn handle_option(
|
||||
let note = quote_option(deprecated.note);
|
||||
let since = quote_option(deprecated.since);
|
||||
|
||||
quote!(Some(ruff_workspace::options_base::Deprecated { since: #since, message: #note }))
|
||||
quote!(Some(crate::options_base::Deprecated { since: #since, message: #note }))
|
||||
} else {
|
||||
quote!(None)
|
||||
};
|
||||
|
||||
Ok(quote_spanned!(
|
||||
ident.span() => {
|
||||
visit.record_field(#renamed_field, ruff_workspace::options_base::OptionField{
|
||||
visit.record_field(#kebab_name, crate::options_base::OptionField{
|
||||
doc: &#doc,
|
||||
default: &#default,
|
||||
value_type: &#value_type,
|
||||
@@ -360,66 +351,3 @@ 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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ 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 }
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
pub use edit::{DocumentKey, NotebookDocument, PositionEncoding, TextDocument};
|
||||
use lsp_types::CodeActionKind;
|
||||
pub use server::Server;
|
||||
pub use server::{Server, Workspace, Workspaces};
|
||||
pub use session::{ClientSettings, DocumentQuery, DocumentSnapshot, Session};
|
||||
pub use workspace::{Workspace, Workspaces};
|
||||
|
||||
#[macro_use]
|
||||
mod message;
|
||||
@@ -17,7 +16,6 @@ mod logging;
|
||||
mod resolve;
|
||||
mod server;
|
||||
mod session;
|
||||
mod workspace;
|
||||
|
||||
pub(crate) const SERVER_NAME: &str = "ruff";
|
||||
pub(crate) const DIAGNOSTIC_NAME: &str = "Ruff";
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
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;
|
||||
@@ -21,6 +24,7 @@ use types::OneOf;
|
||||
use types::TextDocumentSyncCapability;
|
||||
use types::TextDocumentSyncKind;
|
||||
use types::TextDocumentSyncOptions;
|
||||
use types::Url;
|
||||
use types::WorkDoneProgressOptions;
|
||||
use types::WorkspaceFoldersServerCapabilities;
|
||||
|
||||
@@ -30,8 +34,9 @@ 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::workspace::Workspaces;
|
||||
use crate::session::WorkspaceSettingsMap;
|
||||
use crate::PositionEncoding;
|
||||
|
||||
mod api;
|
||||
@@ -442,3 +447,122 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use lsp_types::{ClientCapabilities, FileEvent, NotebookDocumentCellChange, Url};
|
||||
use settings::ResolvedClientSettings;
|
||||
|
||||
use crate::edit::{DocumentKey, DocumentVersion, NotebookDocument};
|
||||
use crate::workspace::Workspaces;
|
||||
use crate::server::Workspaces;
|
||||
use crate::{PositionEncoding, TextDocument};
|
||||
|
||||
pub(crate) use self::capabilities::ResolvedClientCapabilities;
|
||||
|
||||
@@ -11,7 +11,7 @@ use thiserror::Error;
|
||||
pub(crate) use ruff_settings::RuffSettings;
|
||||
|
||||
use crate::edit::LanguageId;
|
||||
use crate::workspace::{Workspace, Workspaces};
|
||||
use crate::server::{Workspace, Workspaces};
|
||||
use crate::{
|
||||
edit::{DocumentKey, DocumentVersion, NotebookDocument},
|
||||
PositionEncoding, TextDocument,
|
||||
|
||||
@@ -5,8 +5,6 @@ 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>;
|
||||
@@ -59,76 +57,27 @@ pub(crate) enum ConfigurationPreference {
|
||||
EditorOnly,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default, OptionsMetadata)]
|
||||
/// This is a direct representation of the settings schema sent by the client.
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
#[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>,
|
||||
|
||||
/// 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""#
|
||||
)]
|
||||
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>,
|
||||
configuration_preference: Option<ConfigurationPreference>,
|
||||
|
||||
/// 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.
|
||||
/// If `true` or [`None`], show syntax errors as 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)]
|
||||
@@ -149,26 +98,14 @@ impl ClientSettings {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default, OptionsMetadata)]
|
||||
/// Settings needed to initialize tracing. These will only be
|
||||
/// read from the global configuration.
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
#[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 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""#
|
||||
)]
|
||||
/// Path to the log file - tildes and environment variables are supported.
|
||||
pub(crate) log_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
@@ -184,31 +121,14 @@ struct WorkspaceSettings {
|
||||
workspace: Url,
|
||||
}
|
||||
|
||||
/// Settings specific to the Ruff linter.
|
||||
#[derive(Debug, Default, Deserialize, OptionsMetadata)]
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[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>>,
|
||||
}
|
||||
|
||||
@@ -223,13 +143,10 @@ impl LintOptions {
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings specific to the Ruff formatter.
|
||||
#[derive(Debug, Default, Deserialize, OptionsMetadata)]
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[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>,
|
||||
}
|
||||
|
||||
@@ -259,38 +176,6 @@ 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)]
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ 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;
|
||||
|
||||
@@ -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 = "c8826fa4d1d9e3cba4c6e578763878b71fa9a10d" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "351d9cf0037be949d17800d0c7b4838e533c2ed6" }
|
||||
similar = { version = "2.5.0" }
|
||||
tracing = { version = "0.1.40" }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user