Compare commits

..

25 Commits

Author SHA1 Message Date
David Peter
bac74a120b Make CallArguments salsa::interned 2025-02-20 21:08:42 +01:00
David Peter
1ab975b142 Make try_call a query 2025-02-20 20:35:57 +01:00
David Peter
92416e1f85 Make try_call_dunder_get a query 2025-02-20 19:13:27 +01:00
David Peter
5ecea4e81f Use or_fall_back_to 2025-02-20 19:13:27 +01:00
David Peter
b3a0353bf2 Use __kwdefaults__ instead of __module__ 2025-02-20 19:13:27 +01:00
David Peter
9953dede9e Attempt to model getattr_static on gradual types 2025-02-20 19:13:27 +01:00
David Peter
fed67170ec Model fallback MethodType => FunctionType 2025-02-20 19:13:27 +01:00
David Peter
cc5270ae9c Remove incorrect __class__ lookup branch in static_member 2025-02-20 19:13:27 +01:00
David Peter
c0fc2796a2 Add doc comment for try_call_dunder_get 2025-02-20 19:13:27 +01:00
David Peter
c5224316c0 Add TODO for builtins.tuple attribute lookups 2025-02-20 19:13:27 +01:00
David Peter
67087c0417 Add FunctionLiteral and BoundMethod to property tests 2025-02-20 19:13:27 +01:00
David Peter
72fe5525ab Wording 2025-02-20 19:13:27 +01:00
David Peter
ff290172d7 Add TODO: Type::member should return Result 2025-02-20 19:13:27 +01:00
David Peter
7673b7265d Return a Result from try_call_dunder_get 2025-02-20 19:13:27 +01:00
David Peter
caca1874ae Add reference to 'Functions and methods' section in the descriptor guide 2025-02-20 19:13:27 +01:00
David Peter
08f4c60660 Properly catch errors to known function calls 2025-02-20 19:13:27 +01:00
David Peter
e86c21e90a Wording and typos 2025-02-20 19:13:27 +01:00
David Peter
c84f1e0c72 Introduce CallArguments::none() 2025-02-20 19:13:27 +01:00
David Peter
d6ae12c05f Fix two typos 2025-02-20 19:13:27 +01:00
David Peter
0743c21811 Remove memoryview as a KnownClass 2025-02-20 19:13:27 +01:00
David Peter
c322baaaef Fix clippy suggestion 2025-02-20 19:13:27 +01:00
David Peter
ce3dcb066c Handle errors in __get__ calls 2025-02-20 19:13:27 +01:00
David Peter
f406835639 Use write!(…) 2025-02-20 19:13:27 +01:00
David Peter
30383d4855 Patch is_assignable_to to add partial support for SupportsIndex 2025-02-20 19:13:27 +01:00
David Peter
c7d97c3cd5 [red-knot] Method calls and descriptor protocol 2025-02-20 19:13:26 +01:00
34 changed files with 1329 additions and 1638 deletions

148
Cargo.lock generated
View File

@@ -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"

View File

@@ -123,7 +123,7 @@ rayon = { version = "1.10.0" }
regex = { version = "1.10.2" }
rustc-hash = { version = "2.0.0" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "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"] }

View File

@@ -15,7 +15,7 @@ use crate::module_name::ModuleName;
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::ast_ids::AstIdsBuilder;
use crate::semantic_index::attribute_assignment::{AttributeAssignment, AttributeAssignments};
use crate::semantic_index::constraint::{PatternConstraintKind, 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();

View File

@@ -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>,

View File

@@ -165,7 +165,7 @@
//! don't actually store these "list of visible definitions" as a vector of [`Definition`].
//! Instead, [`SymbolBindings`] and [`SymbolDeclarations`] are structs which use bit-sets to track
//! definitions (and constraints, in the case of bindings) in terms of [`ScopedDefinitionId`] and
//! [`ScopedConstraintId`], which are indices into the `all_definitions` and `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,

View File

@@ -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);

View File

@@ -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()
}
},
)

View File

@@ -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));
}
}

View File

@@ -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;

View File

@@ -11,7 +11,7 @@ pub(super) use bind::{bind_call, CallBinding};
/// A successfully bound call where all arguments are valid.
///
/// It's guaranteed that the wrapped bindings have no errors.
#[derive(Debug, Clone, PartialEq, Eq)]
#[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 {

View File

@@ -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,

View File

@@ -16,7 +16,7 @@ use ruff_text_size::Ranged;
/// parameters, and any errors resulting from binding the call.
pub(crate) fn bind_call<'db>(
db: &'db dyn Db,
arguments: &CallArguments<'_, 'db>,
arguments: CallArguments<'db>,
signature: &Signature<'db>,
callable_ty: Type<'db>,
) -> CallBinding<'db> {
@@ -38,7 +38,7 @@ pub(crate) fn bind_call<'db>(
None
}
};
for (argument_index, argument) in arguments.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.

View File

@@ -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;

View File

@@ -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]

View File

@@ -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(&copy).finish()
}
}
impl Idx for Atom {
#[inline]
fn new(value: usize) -> Self {
assert!(value <= 0x00ff_ffff);
#[allow(clippy::cast_possible_truncation)]
Self(value as u32)
}
#[inline]
fn index(self) -> usize {
let (index, _) = self.into_index_and_copy();
index as usize
}
}
impl ScopedVisibilityConstraintId {
/// A special ID that is used for an "always true" / "always visible" constraint.
pub(crate) const ALWAYS_TRUE: ScopedVisibilityConstraintId =
@@ -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,

View File

@@ -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 }

View File

@@ -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)>,

View File

@@ -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)?,

View File

@@ -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
...

View File

@@ -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

View File

@@ -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();

View File

@@ -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 | ...
|

View File

@@ -165,7 +165,7 @@ pub(super) fn dataclass_kind<'a>(
/// Returns `true` if the given class has "default copy" semantics.
///
/// For example, Pydantic `BaseModel` and `BaseSettings` 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"]

View File

@@ -31,78 +31,78 @@ RUF012.py:25:26: RUF012 Mutable class attributes should be annotated with `typin
27 | class_variable: ClassVar[list[int]] = []
|
RUF012.py: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
|

View File

@@ -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())
}
}

View File

@@ -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 }

View File

@@ -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";

View File

@@ -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
}
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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)]

View File

@@ -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
}
}

View File

@@ -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;

View File

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