From 1bd788b2dba093265ed0aa113611807d7f2ff744 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Wed, 17 Sep 2025 17:04:52 -0400 Subject: [PATCH 01/15] stash --- Cargo.lock | 160 ++++- Cargo.toml | 1 + crates/extension-zed/src/lib.rs | 18 +- src/analyze.rs | 559 ++++++++++++++++-- src/backend.rs | 75 ++- src/index.rs | 8 + src/index/js.rs | 4 +- src/js.rs | 35 +- src/model.rs | 51 +- src/python.rs | 61 +- src/python/completions.rs | 11 +- src/python/diagnostics.rs | 21 +- src/python/tests.rs | 8 +- src/record.rs | 18 +- src/server.rs | 5 +- src/template.rs | 15 +- src/utils.rs | 73 ++- src/utils/lru.rs | 52 ++ src/xml.rs | 34 +- testing/Cargo.toml | 4 +- testing/fixtures/python_types/.odoo_lsp | 1 + .../python_types/bakery/__manifest__.py | 1 + .../python_types/bakery/models/models.py | 70 +++ testing/src/tests.rs | 4 +- 24 files changed, 1014 insertions(+), 275 deletions(-) create mode 100644 src/utils/lru.rs create mode 100644 testing/fixtures/python_types/.odoo_lsp create mode 100644 testing/fixtures/python_types/bakery/__manifest__.py create mode 100644 testing/fixtures/python_types/bakery/models/models.py diff --git a/Cargo.lock b/Cargo.lock index b307607..aaa15c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,12 +199,49 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "camino" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", +] + [[package]] name = "cc" version = "1.2.30" @@ -303,6 +340,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -371,6 +417,19 @@ dependencies = [ "syn", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -515,6 +574,15 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -920,9 +988,9 @@ dependencies = [ [[package]] name = "iai-callgrind" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c358f67cf086a3a890a72c390dadc371e9151129b1e9b75acc705d30a77197f" +checksum = "0b1e4910d3a9137442723dfb772c32dc10674c4181ca078d2fd227cd5dce9db0" dependencies = [ "bincode", "bindgen", @@ -954,9 +1022,9 @@ dependencies = [ [[package]] name = "iai-callgrind-runner" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6248b44a281f0c22ba57fe41b56f718a8c2cc682a99f270f667503819d758004" +checksum = "b74c9743c00c3bca4aaffc69c87cae56837796cd362438daf354a3f785788c68" dependencies = [ "serde", ] @@ -1178,7 +1246,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e14eda50a3494b3bf7b9ce51c52434a761e383d7238ce1dd5dcec2fbc13e9fb" dependencies = [ - "dashmap", + "dashmap 6.1.0", "hashbrown 0.14.5", ] @@ -1296,6 +1364,21 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "mini-moka" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c325dfab65f261f386debee8b0969da215b3fa0037e74c8a1234db7ba986d803" +dependencies = [ + "crossbeam-channel", + "crossbeam-utils", + "dashmap 5.5.3", + "skeptic", + "smallvec", + "tagptr", + "triomphe", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1398,7 +1481,7 @@ dependencies = [ "anyhow", "bitflags 2.9.1", "const_format", - "dashmap", + "dashmap 6.1.0", "derive_more", "fomat-macros", "futures", @@ -1407,6 +1490,7 @@ dependencies = [ "ignore", "intmap", "lasso", + "mini-moka", "num_enum", "pathdiff", "phf", @@ -1668,6 +1752,17 @@ dependencies = [ "yansi", ] +[[package]] +name = "pulldown-cmark" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" +dependencies = [ + "bitflags 2.9.1", + "memchr", + "unicase", +] + [[package]] name = "qp-trie" version = "0.8.2" @@ -2110,18 +2205,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.225" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" dependencies = [ "proc-macro2", "quote", @@ -2212,6 +2317,21 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "skeptic" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" +dependencies = [ + "bytecount", + "cargo_metadata", + "error-chain", + "glob", + "pulldown-cmark", + "tempfile", + "walkdir", +] + [[package]] name = "slab" version = "0.4.10" @@ -2350,6 +2470,12 @@ dependencies = [ "syn", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tar" version = "0.4.44" @@ -2569,7 +2695,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cd168c085174eafa7492a519715f2d59436dc28cdfd9d13a5b864246899db9" dependencies = [ "bytes", - "dashmap", + "dashmap 6.1.0", "futures", "httparse", "lsp-types 0.97.0", @@ -2722,6 +2848,12 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "triomphe" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" + [[package]] name = "try-lock" version = "0.2.5" @@ -2754,6 +2886,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" version = "1.0.18" diff --git a/Cargo.toml b/Cargo.toml index 24a4999..1dbf368 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,6 +100,7 @@ futures.workspace = true globwalk.workspace = true ts-macros.workspace = true tracing-subscriber.workspace = true +mini-moka = "0.10.3" [dev-dependencies] pretty_assertions.workspace = true diff --git a/crates/extension-zed/src/lib.rs b/crates/extension-zed/src/lib.rs index f6d7ae2..271d864 100644 --- a/crates/extension-zed/src/lib.rs +++ b/crates/extension-zed/src/lib.rs @@ -31,17 +31,15 @@ impl zed::Extension for Extension { fn language_server_command(&mut self, lsid: &zed::LanguageServerId, dir: &zed::Worktree) -> Result { let env = self.prepare_env(dir); - if let Some(binary) = LspSettings::for_worktree("odoo-lsp", dir) - .ok() - .and_then(|settings| settings.binary) + if let Ok(settings) = LspSettings::for_worktree("odoo-lsp", dir) + && let Some(binary) = settings.binary + && let Some(command) = binary.path { - if let Some(command) = binary.path.clone() { - return Ok(zed::Command { - command, - args: binary.arguments.unwrap_or_default(), - env, - }); - } + return Ok(zed::Command { + command, + args: binary.arguments.unwrap_or_default(), + env, + }); } if let Some(command) = dir.which("odoo-lsp") { diff --git a/src/analyze.rs b/src/analyze.rs index b32e670..1e7fe3f 100644 --- a/src/analyze.rs +++ b/src/analyze.rs @@ -2,11 +2,13 @@ //! [`Index::model_of_range`] and [`Index::type_of`]. use std::{ + borrow::Cow, fmt::{Debug, Write}, ops::ControlFlow, - sync::Arc, + sync::{Arc, atomic::Ordering}, }; +use fomat_macros::fomat; use lasso::Spur; use ropey::Rope; use tracing::instrument; @@ -15,9 +17,9 @@ use tree_sitter::{Node, Parser, QueryCursor, StreamingIterator}; use crate::{ ImStr, dig, format_loc, index::{_G, _I, _R, Index, Symbol}, - model::{Method, MethodReturnType, ModelEntry, ModelName, PropertyKind}, + model::{Method, ModelName, PropertyInfo}, test_utils, - utils::{ByteRange, PreTravel, RangeExt, TryResultExt, rope_conv}, + utils::{ByteOffset, ByteRange, Defer, PreTravel, RangeExt, TryResultExt, rope_conv}, }; use ts_macros::query; @@ -49,7 +51,7 @@ pub static MODEL_METHODS: phf::Set<&str> = phf::phf_set!( ); /// The subset of types that may resolve to a model. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Type { Env, /// \*.env.ref() @@ -63,10 +65,47 @@ pub enum Type { Method(ModelName, ImStr), /// `odoo.http.request` HttpRequest, + Dict(Box<[Type; 2]>), + /// A bag of enumerated properties and their types + DictBag(Vec<(DictKey, Type)>), + /// Equivalent to Value, but may have a better semantic name + PyBuiltin(ImStr), + List(ListElement), /// Can never be resolved, useful for non-model bindings. Value, } +#[derive(Clone, PartialEq, Eq)] +pub enum ListElement { + Vacant, + Occupied(Box), +} + +impl Debug for ListElement { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Vacant => f.write_str("..."), + Self::Occupied(inner) => inner.fmt(f), + } + } +} + +#[derive(Clone, PartialEq, Eq)] +pub enum DictKey { + String(ImStr), + Type(Type), +} + +impl Debug for DictKey { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::String(key) => key.fmt(f), + Self::Type(key) => key.fmt(f), + } + } +} + #[derive(Clone, Debug)] pub enum FunctionParam { Param(ImStr), @@ -138,6 +177,33 @@ query! { (#match? @_mapped "^(mapp|filter|sort)ed$")) } +#[rustfmt::skip] +query! { + PythonBuiltinCall(Append, AppendList, AppendMap, AppendMapKey, AppendValue, UpdateMap, UpdateArgs); +// value.append(...) OR value['foobar'].append(...) +(call + (attribute + (subscript (identifier) @APPEND_MAP (string (string_content) @APPEND_MAP_KEY)) + (identifier) @_append) + (argument_list . (_) @APPEND_VALUE) + (#eq? @_append "append")) + +(call + (attribute + (identifier) @APPEND_LIST + (identifier) @APPEND) + (argument_list . (_) @APPEND_VALUE) + (#eq? @_append "append")) + +// value.update(...) +(call + (attribute + (identifier) @UPDATE_MAP + (identifier) @_update) + (argument_list) @UPDATE_ARGS + (#eq? @_update "update")) +} + pub type ScopeControlFlow = ControlFlow, bool>; impl Index { #[inline] @@ -204,6 +270,23 @@ impl Index { { let lhs = &contents[lhs.byte_range()]; scope.insert(lhs.to_string(), type_); + } else if lhs.kind() == "subscript" + && let Some(map) = dig!(lhs, identifier) + && let Some(key) = dig!(lhs, string(1).string_content(1)) + && let Some(rhs) = lhs.next_named_sibling() + && let type_ = self.type_of(rhs, scope, contents) + && let Some(Type::DictBag(properties)) = scope.variables.get_mut(&contents[map.byte_range()]) + { + let type_ = type_.unwrap_or(Type::Value); + let key = &contents[key.byte_range()]; + if let Some(idx) = properties.iter().position(|(prop, _)| match prop { + DictKey::String(prop) => prop.as_str() == key, + DictKey::Type(_) => false, + }) { + properties[idx].1 = type_; + } else { + properties.push((DictKey::String(ImStr::from(key)), type_)); + } } } "for_statement" => { @@ -269,6 +352,85 @@ impl Index { scope.insert(iter.to_string(), type_); } } + "call" => { + let query = PythonBuiltinCall::query(); + let mut cursor = QueryCursor::new(); + let mut matches = cursor.matches(query, node, contents.as_bytes()); + let Some(call) = matches.next() else { + return ControlFlow::Continue(false); + }; + if let Some(value) = call.nodes_for_capture_index(PythonBuiltinCall::AppendValue as _).next() + && let Some(value) = self.type_of(value, scope, contents) + { + if let Some(list) = call.nodes_for_capture_index(PythonBuiltinCall::AppendList as _).next() { + if let Some(Type::List(slot @ ListElement::Vacant)) = + scope.variables.get_mut(&contents[list.byte_range()]) + { + *slot = ListElement::Occupied(Box::new(value)); + } + } else if let Some(map) = call.nodes_for_capture_index(PythonBuiltinCall::AppendMap as _).next() { + let Some(key) = call + .nodes_for_capture_index(PythonBuiltinCall::AppendMapKey as _) + .next() + else { + return ControlFlow::Continue(false); + }; + let key = &contents[key.byte_range()]; + + if let Some(Type::DictBag(properties)) = scope.variables.get_mut(&contents[map.byte_range()]) + && let Some((_, Type::List(slot @ ListElement::Vacant))) = + properties.iter_mut().find(|(prop, _)| match prop { + DictKey::String(prop) => prop.as_str() == key, + DictKey::Type(_) => false, + }) { + *slot = ListElement::Occupied(Box::new(value)); + } + } + } else if let Some(map) = call.nodes_for_capture_index(PythonBuiltinCall::UpdateMap as _).next() { + let Some(Type::DictBag(properties)) = scope.variables.get_mut(&contents[map.byte_range()]) else { + return ControlFlow::Continue(false); + }; + let Some(args) = call.nodes_for_capture_index(PythonBuiltinCall::UpdateArgs as _).next() else { + return ControlFlow::Continue(false); + }; + + let mut properties = core::mem::take(properties); + let mut cursor = args.walk(); + let mut children = args.named_children(&mut cursor); + if let Some(first) = children.by_ref().next() + && let Some(Type::DictBag(update_props)) = self.type_of(first, scope, contents) + { + properties.extend(update_props); + } + + for named_arg in children { + if named_arg.kind() == "keyword_argument" + && let Some(name) = named_arg.child_by_field_name("name") + && let Some(value) = named_arg.child_by_field_name("value") + { + let key = &contents[name.byte_range()]; + let type_ = self.type_of(value, scope, contents).unwrap_or(Type::Value); + if let Some(idx) = properties.iter().position(|(prop, _)| match prop { + DictKey::String(prop) => prop.as_str() == key, + DictKey::Type(_) => false, + }) { + properties[idx].1 = type_; + } else { + properties.push((DictKey::String(ImStr::from(key)), type_)); + } + } else if named_arg.kind() == "dictionary_splat" + && let Some(value) = named_arg.named_child(0) + && let Some(Type::DictBag(update_props)) = self.type_of(value, scope, contents) + { + properties.extend(update_props); + } + } + + scope + .variables + .insert(contents[map.byte_range()].to_string(), Type::DictBag(properties)); + } + } "with_statement" => { // with Form(self.env['..']) as alias: // TODO: Support more structures as needed @@ -343,12 +505,54 @@ impl Index { Type::Env if rhs.kind() == "string" => { Some(Type::Model(contents[rhs.byte_range().shrink(1)].into())) } + Type::Env => Some(Type::Model(ImStr::from_static("_unknown"))), Type::Model(_) | Type::Record(_) => Some(obj_ty), + Type::Dict(dict) => { + let [key, value] = *dict; + let rhs = self.type_of(rhs, scope, contents); + // FIXME: We trust that the user makes the correct judgment here and returns the type requested. + rhs.is_none_or(|lhs| lhs == key).then_some(value) + } + Type::DictBag(properties) => { + // compare by key + if let Some(rhs) = dig!(rhs, string_content(1)) { + let rhs = &contents[rhs.byte_range()]; + for (key, value) in properties { + match key { + DictKey::String(key) if key.as_str() == rhs => { + return Some(value); + } + DictKey::String(_) | DictKey::Type(_) => {} + } + } + return None; + } + + // compare by type + let rhs = self.type_of(rhs, scope, contents)?; + for (key, value) in properties { + match key { + DictKey::Type(key) if key == rhs => return Some(value), + DictKey::Type(_) | DictKey::String(_) => {} + } + } + + None + } + // FIXME: Again, just trust that the user is doing the right thing. + Type::List(ListElement::Occupied(slot)) => Some(*slot), _ => None, } } "attribute" => self.type_of_attribute_node(node, scope, contents), "identifier" => { + if let Some(parent) = node.parent() + && parent.kind() == "attribute" + && parent.named_child(0).unwrap() != node + { + return self.type_of_attribute_node(parent, scope, contents); + } + let key = &contents[node.byte_range()]; if key == "super" { return Some(Type::Super); @@ -366,10 +570,91 @@ impl Index { self.type_of(rhs, scope, contents) } "call" => self.type_of_call_node(node, scope, contents), - "binary_operator" | "boolean_operator" => { - // (_ left right) + "binary_operator" => { + if let Some(left) = node.child_by_field_name("left") + && let Some(left) = self.type_of(left, scope, contents) + { + return Some(left); + } + + self.type_of(node.child_by_field_name("right")?, scope, contents) + } + "boolean_operator" => { + if let Some(left) = node.child_by_field_name("right") + && let Some(left) = self.type_of(left, scope, contents) + { + return Some(left); + } + self.type_of(node.child_by_field_name("left")?, scope, contents) } + "dictionary_comprehension" => { + let pair = dig!(node, pair)?; + let mut comprehension_scope; + let mut pair_scope = scope; + if let Some(for_in_clause) = dig!(node, for_in_clause(1)) + && let Some(scrutinee) = dig!(for_in_clause, identifier) // TODO: pattern_list for `for a, b, ...` + && let Some(iteratee) = for_in_clause.child_by_field_name("right") + && let Some(iter_ty) = self.type_of(iteratee, scope, contents) + { + // FIXME: How to prevent this clone? + comprehension_scope = Scope::new(Some(scope.clone())); + comprehension_scope.insert(contents[scrutinee.byte_range()].to_string(), iter_ty); + pair_scope = &comprehension_scope; + } + let lhs = pair + .named_child(0) + .and_then(|lhs| self.type_of(lhs, pair_scope, contents)); + let rhs = pair + .named_child(1) + .and_then(|lhs| self.type_of(lhs, pair_scope, contents)); + if lhs.is_some() || rhs.is_some() { + Some(Type::Dict(Box::new([ + lhs.unwrap_or(Type::Value), + rhs.unwrap_or(Type::Value), + ]))) + } else { + None + } + } + "dictionary" => { + let mut properties = vec![]; + for child in node.named_children(&mut node.walk()) { + if child.kind() == "pair" + && let Some(lhs) = child.child_by_field_name("key") + && let Some(rhs) = child.child_by_field_name("value") + { + let key; + if let Some(lhs) = dig!(lhs, string_content(1)) { + key = DictKey::String(ImStr::from(&contents[lhs.byte_range()])); + } else if matches!(lhs.kind(), "true" | "false" | "string" | "none" | "float" | "integer") { + key = DictKey::Type(Type::PyBuiltin(ImStr::from(&contents[lhs.byte_range()]))); + } else if let Some(lhs) = self.type_of(lhs, scope, contents) { + key = DictKey::Type(lhs); + } else { + continue; + } + + let value = self.type_of(rhs, scope, contents).unwrap_or(Type::Value); + properties.push((key, value)); + } + } + Some(Type::DictBag(properties)) + } + "list" => { + let mut slot = ListElement::Vacant; + for child in node.named_children(&mut node.walk()) { + if let Some(child) = self.type_of(child, scope, contents) { + slot = ListElement::Occupied(Box::new(child)); + break; + } + } + Some(Type::List(slot)) + } + "string" => Some(Type::PyBuiltin(ImStr::from_static("str"))), + "integer" => Some(Type::PyBuiltin(ImStr::from_static("int"))), + "float" => Some(Type::PyBuiltin(ImStr::from_static("float"))), + "true" | "false" | "comparison_operator" => Some(Type::PyBuiltin(ImStr::from_static("bool"))), _ => None, } } @@ -416,12 +701,72 @@ impl Index { } Type::Method(model, method) => { let method = _G(&method)?; - let ret_model = self.resolve_method_returntype(method.into(), *model)?; - Some(Type::Model(_R(ret_model).into())) + let args = self.prepare_call_scope(model, method.into(), call, scope, contents); + self.eval_method_rtype(method.into(), *model, args) } - Type::Env | Type::Record(..) | Type::Model(..) | Type::HttpRequest | Type::Value => None, + Type::Env + | Type::Record(..) + | Type::Model(..) + | Type::HttpRequest + | Type::Value + | Type::PyBuiltin(..) + | Type::Dict(..) + | Type::DictBag(..) + | Type::List(..) => None, } } + #[instrument(skip_all, fields(model, method))] + fn prepare_call_scope( + &self, + model: ModelName, + method: Symbol, + call: Node, + scope: &Scope, + contents: &str, + ) -> Option { + // (call + // (arguments_list + // (_) + // (keyword_argument (identifier) (_)))) + let arguments_list = dig!(call, argument_list(1))?; + + let model = self.models.populate_properties(model, &[])?; + let method = model.methods.as_ref()?.get(&method)?; + let arguments = method.arguments.clone().unwrap_or_default(); + if arguments.is_empty() { + return None; + } + + drop(model); + let mut out = Scope::new(None); + for (idx, arg) in arguments_list.named_children(&mut arguments_list.walk()).enumerate() { + if arg.kind() == "keyword_argument" + && let Some(key) = arg.child_by_field_name("key") + && let Some(value) = arg.child_by_field_name("value") + { + let key = &contents[key.byte_range()]; + if !arguments.iter().any(|arg| match arg { + FunctionParam::Named(arg) => arg.as_str() == key, + _ => false, + }) { + continue; + } + let Some(value) = self.type_of(value, scope, contents) else { + continue; + }; + out.insert(key.to_string(), value); + } else if let Some(FunctionParam::Param(argname)) = arguments.get(idx) + && let Some(value) = self.type_of(arg, scope, contents) + { + out.insert(argname.to_string(), value); + } else { + continue; + } + } + + Some(out) + } + #[instrument(skip_all, ret)] fn type_of_attribute_node(&self, attribute: Node<'_>, scope: &Scope, contents: &str) -> Option { let lhs = attribute.named_child(0)?; let lhs = self.type_of(lhs, scope, contents)?; @@ -448,18 +793,55 @@ impl Index { _ => None, } } + #[instrument(skip_all, fields(attr=attr), ret)] pub fn type_of_attribute(&self, type_: &Type, attr: &str, scope: &Scope) -> Option { let model = self.try_resolve_model(type_, scope)?; let model_entry = self.models.populate_properties(model, &[])?; - let attr_key = _G(attr)?; - let attr_kind = model_entry.prop_kind(attr_key)?; - match attr_kind { - PropertyKind::Field => { - drop(model_entry); - let relation = self.models.resolve_related_field(attr_key.into(), model.into())?; - Some(Type::Model(_R(relation).into())) - } - PropertyKind::Method => Some(Type::Method(model, attr.into())), + if let Some(attr_key) = _G(attr) + && let Some(attr_kind) = model_entry.prop_kind(attr_key) + { + match attr_kind { + PropertyInfo::Field(type_) => { + drop(model_entry); + if let Some(relation) = self.models.resolve_related_field(attr_key.into(), model.into()) { + return Some(Type::Model(_R(relation).into())); + } + + match _R(type_) { + "Selection" | "Char" | "Text" | "Html" => Some(Type::PyBuiltin(ImStr::from_static("str"))), + "Integer" => Some(Type::PyBuiltin(ImStr::from_static("int"))), + "Float" | "Monetary" => Some(Type::PyBuiltin(ImStr::from_static("float"))), + "Date" => Some(Type::PyBuiltin(ImStr::from_static("date"))), + "Datetime" => Some(Type::PyBuiltin(ImStr::from_static("datetime"))), + _ => None, + } + } + PropertyInfo::Method => Some(Type::Method(model, attr.into())), + } + } else { + match attr { + "id" if matches!(type_, Type::Model(..) | Type::Record(..)) => { + Some(Type::PyBuiltin(ImStr::from_static("int"))) + } + "ids" if matches!(type_, Type::Model(..) | Type::Record(..)) => Some(Type::List( + ListElement::Occupied(Box::new(Type::PyBuiltin(ImStr::from_static("int")))), + )), + "display_name" if matches!(type_, Type::Model(..) | Type::Record(..)) => { + Some(Type::PyBuiltin(ImStr::from_static("str"))) + } + "create_date" | "write_date" if matches!(type_, Type::Model(..) | Type::Record(..)) => { + Some(Type::PyBuiltin(ImStr::from_static("datetime"))) + } + "create_uid" | "write_uid" if matches!(type_, Type::Model(..) | Type::Record(..)) => { + Some(Type::Model(ImStr::from_static("res.users"))) + } + "_fields" if matches!(type_, Type::Model(..) | Type::Record(..)) => Some(Type::Dict(Box::new([ + Type::PyBuiltin(ImStr::from_static("str")), + Type::Model(ImStr::from_static("ir.model.fields")), + ]))), + "env" if matches!(type_, Type::Model(..) | Type::Record(..) | Type::HttpRequest) => Some(Type::Env), + _ => None, + } } } pub fn has_attribute(&self, type_: &Type, attr: &str, scope: &Scope) -> bool { @@ -485,6 +867,60 @@ impl Index { _ => None, } } + pub fn type_display<'a>(&self, type_: &'a Type) -> Option> { + match type_ { + Type::Dict(pair) => { + let [lhs, rhs] = &**pair; + let lhs = self.type_display(lhs); + let lhs = lhs.as_deref().unwrap_or("..."); + let rhs = self.type_display(rhs); + let rhs = rhs.as_deref().unwrap_or("..."); + Some(fomat! { "dict[" (lhs) ", " (rhs) "]" }.into()) + } + Type::DictBag(properties) => { + let properties_fragment = fomat! { + for (key, value) in properties { + " " + match key { + DictKey::String(key) => { "\"" (key) "\"" } + DictKey::Type(Type::Dict(..) | Type::DictBag(..)) => { "{...}" } + DictKey::Type(key) => { (self.type_display(key).as_deref().unwrap_or("...")) } + } ": " (self.type_display(value).as_deref().unwrap_or("...")) + } sep { ",\n" } + }; + Some( + fomat! { + if !properties.is_empty() { + "{\n" (properties_fragment) "\n}" + } else { + "{}" + } + } + .into(), + ) + } + Type::PyBuiltin(builtin) => Some(builtin.as_str().into()), + Type::List(slot) => { + let slot = match slot { + ListElement::Vacant => None, + ListElement::Occupied(slot) => self.type_display(slot), + }; + Some(match slot { + Some(slot) => format!("list[{slot}]").into(), + None => "list".into(), + }) + } + Type::Env => Some("Environment".into()), + Type::Model(model) => Some(format!(r#"Model["{model}"]"#).into()), + Type::Record(xml_id) => { + let xml_id = _G(xml_id)?; + let record = self.records.get(&xml_id.into())?; + Some(_R(record.model?).into()) + } + Type::Method(..) => unreachable!("Bug: this function should not handle methods"), + Type::RefFn | Type::ModelFn(_) | Type::Super | Type::HttpRequest | Type::Value => None, + } + } /// Iterates depth-first over `node` using [`PreTravel`]. Automatically calls [`Scope::exit`] at suitable points. /// /// [`ControlFlow::Continue`] accepts a boolean to indicate whether [`Scope::enter`] was called. @@ -519,29 +955,57 @@ impl Index { (scope, None) } /// Resolves the return type of a method as well as populating its arguments and docstring. + /// + /// `parameters` can be provided using [`Index::prepare_call_scope`]. #[instrument(level = "trace", ret, skip(self, model), fields(model = _R(model)))] - pub fn resolve_method_returntype(&self, method: Symbol, model: Spur) -> Option> { + pub fn eval_method_rtype(&self, method: Symbol, model: Spur, parameters: Option) -> Option { _ = self.models.populate_properties(model.into(), &[]); let mut model_entry = self.models.get_mut(&model.into())?; let method_obj = model_entry.methods.as_mut()?.get_mut(&method)?; - match method_obj.return_type { - MethodReturnType::Unprocessed => {} - MethodReturnType::Value | MethodReturnType::Processing => return None, - MethodReturnType::Relational(rel) => return Some(rel), + + if method_obj + .pending_eval + .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) + .is_err() + { + return None; } + let _guard = Defer(Some(|| { + if let Some(model_entry) = self.models.get_mut(&model.into()) + && let Some(methods) = model_entry.methods.as_ref() + && let Some(method) = methods.get(&method) + { + method.pending_eval.store(false, Ordering::Relaxed); + } + })); + let location = method_obj.locations.first().cloned()?; - Arc::make_mut(method_obj).return_type = MethodReturnType::Processing; drop(model_entry); - let contents = test_utils::fs::read_to_string(location.path.to_path()).unwrap(); - let rope = Rope::from_str(&contents); - let rope = rope.slice(..); - - let mut parser = Parser::new(); - parser.set_language(&tree_sitter_python::LANGUAGE.into()).unwrap(); - let range: ByteRange = rope_conv(location.range, rope).ok()?; - let ast = parser.parse(contents.as_bytes(), None)?; + let ast; + let contents; + let end_offset: ByteOffset; + let path = location.path.to_path(); + if let Some(cached) = self.ast_cache.get(&path) { + end_offset = rope_conv(location.range.end, cached.rope.slice(..)); + ast = cached.tree.clone(); + contents = String::from(cached.rope.clone()); + } else { + contents = test_utils::fs::read_to_string(location.path.to_path()).unwrap(); + let rope = Rope::from_str(&contents); + end_offset = rope_conv(location.range.end, rope.slice(..)); + let mut parser = Parser::new(); + parser.set_language(&tree_sitter_python::LANGUAGE.into()).unwrap(); + ast = parser.parse(contents.as_bytes(), None)?; + self.ast_cache.insert( + path, + Arc::new(crate::index::AstCacheItem { + tree: ast.clone(), + rope, + }), + ); + } // TODO: Improve this heuristic fn is_toplevel_return(mut node: Node) -> bool { @@ -566,8 +1030,8 @@ impl Index { node.parent().is_some_and(is_block_of_class) } - let (self_type, fn_scope, self_param) = determine_scope(ast.root_node(), &contents, range.end.0)?; - let mut scope = Scope::default(); + let (self_type, fn_scope, self_param) = determine_scope(ast.root_node(), &contents, end_offset.0)?; + let mut scope = parameters.unwrap_or_default(); let self_type = match self_type { Some(type_) => &contents[type_.byte_range().shrink(1)], None => "", @@ -585,10 +1049,11 @@ impl Index { let Some(type_) = self.type_of(child, scope, &contents) else { return ControlFlow::Continue(entered); }; - let Some(resolved) = self.try_resolve_model(&type_, scope) else { - return ControlFlow::Continue(entered); + + return match self.try_resolve_model(&type_, scope) { + Some(resolved) => ControlFlow::Break(Some(Type::Model(ImStr::from(_R(resolved))))), + None => ControlFlow::Break(Some(type_)), }; - return ControlFlow::Break(Some(resolved)); } ControlFlow::Continue(entered) @@ -596,10 +1061,6 @@ impl Index { let mut model = self.models.try_get_mut(&model.into()).expect(format_loc!("deadlock"))?; let method = Arc::make_mut(model.methods.as_mut()?.get_mut(&method)?); - match type_ { - Some(rel) => method.return_type = MethodReturnType::Relational(rel), - None => method.return_type = MethodReturnType::Value, - } let docstring = Self::parse_method_docstring(fn_scope, &contents) .map(|doc| ImStr::from(Method::postprocess_docstring(doc))); @@ -633,6 +1094,7 @@ impl Index { method.arguments = Some(args.collect()); } + method.pending_eval.store(false, Ordering::Release); type_ } fn parse_method_docstring<'out>(fn_scope: Node, contents: &'out str) -> Option<&'out str> { @@ -697,8 +1159,9 @@ mod tests { use tower_lsp_server::lsp_types::Position; use tree_sitter::{Parser, QueryCursor, StreamingIterator, StreamingIteratorMut}; - use crate::analyze::FieldCompletion; - use crate::index::{_I, _R}; + use crate::ImStr; + use crate::analyze::{FieldCompletion, Type}; + use crate::index::_I; use crate::utils::{ByteOffset, acc_vec, rope_conv}; use crate::{index::Index, test_utils::cases::foo::prepare_foo_index}; @@ -756,7 +1219,7 @@ class Foo(models.Model): "#; let ast = parser.parse(contents, None).unwrap(); let rope = Rope::from(contents); - let fn_start: ByteOffset = rope_conv(Position { line: 3, character: 1 }, rope.slice(..)).unwrap(); + let fn_start: ByteOffset = rope_conv(Position { line: 3, character: 1 }, rope.slice(..)); let fn_scope = ast .root_node() .named_descendant_for_byte_range(fn_start.0, fn_start.0) @@ -773,8 +1236,8 @@ class Foo(models.Model): }; assert_eq!( - index.resolve_method_returntype(_I("test").into(), _I("bar")).map(_R), - Some("foo") + index.eval_method_rtype(_I("test").into(), _I("bar"), None), + Some(Type::Model(ImStr::from_static("foo"))) ) } @@ -786,8 +1249,8 @@ class Foo(models.Model): }; assert_eq!( - index.resolve_method_returntype(_I("test").into(), _I("quux")).map(_R), - Some("foo") + index.eval_method_rtype(_I("test").into(), _I("quux"), None), + Some(Type::Model(ImStr::from_static("foo"))) ) } } diff --git a/src/backend.rs b/src/backend.rs index 9944641..ab0bd72 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -19,6 +19,7 @@ use tower_lsp_server::Client; use tower_lsp_server::{UriExt, lsp_types::*}; use tree_sitter::{Parser, Tree}; +use crate::analyze::{Scope, Type}; use crate::prelude::*; use crate::component::{Prop, PropDescriptor}; @@ -341,10 +342,10 @@ impl Backend { for change in delta { // TODO: Handle full text changes in delta let range = ok!(change.range, "delta without range"); - let start: ByteOffset = ok!(rope_conv(range.start, rope), "delta start"); + let start: ByteOffset = rope_conv(range.start, rope); // The old rope is used to calculate the *old* range-end, because // the diff may have caused it to fall out of the new rope's bounds. - let end: ByteOffset = ok!(rope_conv(range.end, old_rope), "delta end"); + let end: ByteOffset = rope_conv(range.end, old_rope); let len_new = change.text.len(); let start_position = tree_sitter::Point { row: range.start.line as usize, @@ -356,7 +357,7 @@ impl Backend { }; // calculate new_end_position using rope let new_end_offset = ByteOffset(start.0 + len_new); - let new_end_position: Position = ok!(rope_conv(new_end_offset, rope), "new_end_position"); + let new_end_position: Position = rope_conv(new_end_offset, rope); let new_end_position = tree_sitter::Point { row: new_end_position.line as usize, column: new_end_position.character as usize, @@ -561,7 +562,7 @@ impl Index { if !items.has_space() { return Ok(()); } - let range = ok!(rope_conv(range, rope), "(complete_xml_id) range"); + let range = rope_conv(range, rope); let Ok(by_prefix) = self.records.by_prefix.try_read() else { return Ok(()); }; @@ -644,7 +645,7 @@ impl Index { return Ok(()); } let model_key = _I(&model); - let range = ok!(rope_conv(range.clone(), rope), "range={:?}", range); + let range = rope_conv(range.clone(), rope); let Some(model_entry) = self.models.populate_properties(model_key.into(), &[]) else { return Ok(()); }; @@ -719,7 +720,7 @@ impl Index { ) -> anyhow::Result<()> { let component = ok!(_G(component), "(complete_component_prop) component"); let component = ok!(self.components.get(&component.into()), "component"); - let range = ok!(rope_conv(range, rope), "(complete_component_prop) range"); + let range = rope_conv(range, rope); let completions = component.props.iter().map(|(prop, desc)| { let prop = _R(prop); CompletionItem { @@ -773,14 +774,15 @@ impl Index { let method = _G(&completion.label)?; let model_key = _G(&model)?; let method_name = _R(method); - let rtype = self.resolve_method_returntype(method.into(), model_key).map(_R); + let rtype = self.eval_method_rtype(method.into(), model_key, None); + let rtype = rtype.as_ref().and_then(|rtype| self.type_display(rtype)); let entry = self.models.get(&model_key.into())?; let methods = entry.methods.as_ref()?; let method_entry = methods.get(&method.into())?; completion.documentation = Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, - value: self.method_docstring(method_name, method_entry, rtype), + value: self.method_docstring(method_name, method_entry, rtype.as_deref()), })); Some(()) @@ -813,10 +815,7 @@ impl Index { } } }; - let rtype = match rtype { - Some(type_) => format!("Model[\"{type_}\"]"), - None => "...".to_string(), - }; + let rtype = rtype.unwrap_or("..."); let params_fragment = match method.arguments.as_deref() { None => "...".to_string(), Some([]) => String::new(), @@ -831,11 +830,35 @@ impl Index { fomat! { "```python\n" "(method) def " (name) "(" (params_fragment) ") -> " (rtype) - "\n" - "```\n" + "\n```\n" (origin_fragment) } } + #[instrument(skip_all, fields(name, type_))] + pub fn hover_variable( + &self, + name: Option<&str>, + type_: Type, + range: Option, + ) -> anyhow::Result> { + let type_fragment = match type_ { + Type::Method(model, method) => return self.hover_property_name(&method, _R(model), range), + _ => self.type_display(&type_), + }; + let type_fragment = type_fragment.as_deref().unwrap_or("Unknown"); + let value = fomat! { + "```py\n" + if let Some(name) = name { "(variable) " (name) ": " } (type_fragment) + "\n```" + }; + Ok(Some(Hover { + range, + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value, + }), + })) + } pub fn completion_resolve_field(&self, completion: &mut CompletionItem) -> Option<()> { let CompletionData { model } = completion .data @@ -887,11 +910,13 @@ impl Index { if let Some(help) = &field.help { (help.to_string()) } } } + #[instrument(skip_all, fields(name, model))] pub fn hover_property_name(&self, name: &str, model: &str, range: Option) -> anyhow::Result> { let model_key = _I(model); let entry = some!(self.models.populate_properties(model_key.into(), &[])); - let prop = some!(_G(name)); - if let Some(ref fields) = entry.fields + let prop = _G(name); + if let Some(prop) = prop + && let Some(ref fields) = entry.fields && let Some(field) = fields.get(&prop.into()) { Ok(Some(Hover { @@ -901,22 +926,26 @@ impl Index { value: self.field_docstring(field, true), }), })) - } else if let Some(ref methods) = entry.methods + } else if let Some(prop) = prop + && let Some(ref methods) = entry.methods && methods.contains_key(&prop.into()) { drop(entry); - let rtype = self.resolve_method_returntype(prop.into(), model_key); + let rtype = self.eval_method_rtype(prop.into(), model_key, None); + let rtype = rtype.as_ref().and_then(|rtype| self.type_display(rtype)); let model = self.models.get(&model_key.into()).unwrap(); let method = model.methods.as_ref().unwrap().get(&prop.into()).unwrap(); Ok(Some(Hover { range, contents: HoverContents::Markup(MarkupContent { kind: MarkupKind::Markdown, - value: self.method_docstring(name, method, rtype.map(_R)), + value: self.method_docstring(name, method, rtype.as_deref()), }), })) } else { - Ok(None) + drop(entry); + let attr_type = some!(self.type_of_attribute(&Type::Model(ImStr::from(model)), name, &Scope::new(None))); + self.hover_variable(Some(name), attr_type, range) } } /// Returns a Markdown-formatted docstring for a model. @@ -1144,7 +1173,7 @@ impl Index { rope: RopeSlice<'_>, items: &mut MaxVec, ) -> anyhow::Result<()> { - let range = ok!(rope_conv(range, rope)); + let range = rope_conv(range, rope); let completions = self.actions.iter().flat_map(|tag| { let tag = tag.key().to_string(); Some(CompletionItem { @@ -1186,7 +1215,7 @@ impl Index { rope: RopeSlice<'_>, items: &mut MaxVec, ) -> anyhow::Result<()> { - let range = ok!(rope_conv(range, rope)); + let range = rope_conv(range, rope); let completions = self.widgets.iter().flat_map(|widget| { let widget = widget.key().to_string(); Some(CompletionItem { @@ -1219,7 +1248,7 @@ impl Text { out = NULL_RANGE; continue; }; - let range: ByteRange = rope_conv(range, rope).ok()?; + let range: ByteRange = rope_conv(range, rope); out = out.start.min(range.start.0)..out.end.max(range.end.0); } debug!("(damage_zone)\nseed={seed:?}\n out={out:?}"); diff --git a/src/index.rs b/src/index.rs index e6247ab..c352b2a 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1,5 +1,6 @@ //! The main indexer for all language items, including [`Record`]s, QWeb [`Template`]s, and Owl [`Component`]s +use mini_moka::sync::Cache; use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -78,6 +79,13 @@ pub struct Index { /// Cache for transitive dependencies to avoid recalculation #[default(_code = "DashMap::with_shard_amount(4)")] pub(crate) transitive_deps_cache: DashMap>, + #[default(_code = "Cache::new(16)")] + pub(crate) ast_cache: Cache>, +} + +pub struct AstCacheItem { + pub tree: tree_sitter::Tree, + pub rope: Rope, } pub type ModuleName = Symbol; diff --git a/src/index/js.rs b/src/index/js.rs index 18ca6e0..a00d483 100644 --- a/src/index/js.rs +++ b/src/index/js.rs @@ -177,7 +177,7 @@ pub(super) async fn add_root_js(root: Spur, pathbuf: PathBuf) -> anyhow::Result< type_: Default::default(), location: MinLoc { path, - range: rope_conv(range.map_unit(ByteOffset), rope).unwrap(), + range: rope_conv(range.map_unit(ByteOffset), rope), }, }), }; @@ -200,7 +200,7 @@ pub(super) async fn add_root_js(root: Spur, pathbuf: PathBuf) -> anyhow::Result< Some(JsQuery::TemplateInline) => { let Some(component) = &mut component else { continue }; let range = capture.node.byte_range().shrink(1).map_unit(ByteOffset); - component.template = Some(ComponentTemplate::Inline(rope_conv(range, rope).unwrap())); + component.template = Some(ComponentTemplate::Inline(rope_conv(range, rope))); } Some(JsQuery::Subcomponent) => { let Some(component) = &mut component else { continue }; diff --git a/src/js.rs b/src/js.rs index f65ed1d..a0e3b70 100644 --- a/src/js.rs +++ b/src/js.rs @@ -52,9 +52,7 @@ impl Backend { .ast_map .get(file_path.to_str().unwrap()) .ok_or_else(|| errloc!("Did not build AST for {}", uri.path().as_str()))?; - let Ok(ByteOffset(offset)) = rope_conv(params.text_document_position_params.position, rope) else { - Err(errloc!("could not find offset for {}", uri.path().as_str()))? - }; + let ByteOffset(offset) = rope_conv(params.text_document_position_params.position, rope); let contents = Cow::from(rope); // try templates first @@ -123,9 +121,7 @@ impl Backend { .ast_map .get(file_path.to_str().unwrap()) .ok_or_else(|| errloc!("Did not build AST for {}", uri.path().as_str()))?; - let Ok(ByteOffset(offset)) = rope_conv(params.text_document_position.position, rope) else { - Err(errloc!("could not find offset for {}", uri.path().as_str()))? - }; + let ByteOffset(offset) = rope_conv(params.text_document_position.position, rope); let contents = Cow::from(rope); let query = JsQuery::query(); let mut cursor = QueryCursor::new(); @@ -157,9 +153,7 @@ impl Backend { .ast_map .get(file_path.to_str().unwrap()) .ok_or_else(|| errloc!("Did not build AST for {}", uri.path().as_str()))?; - let Ok(ByteOffset(offset)) = rope_conv(params.text_document_position_params.position, rope) else { - return Err(errloc!("could not find offset for {}", uri.path().as_str())); - }; + let ByteOffset(offset) = rope_conv(params.text_document_position_params.position, rope); let contents = Cow::from(rope); let query = JsQuery::query(); let mut cursor = QueryCursor::new(); @@ -206,9 +200,7 @@ impl Backend { { let range = range.shrink(1); let model = &contents[range.clone()]; - return self - .index - .hover_model(model, rope_conv(range.map_unit(ByteOffset), rope).ok(), false, None); + return (self.index).hover_model(model, Some(rope_conv(range.map_unit(ByteOffset), rope)), false, None); } if let Some(model_node) = model_arg_node @@ -219,9 +211,11 @@ impl Backend { let range = range.shrink(1); let model = &contents[model_node.byte_range().shrink(1)]; let method = &contents[range.clone()]; - return self - .index - .hover_property_name(method, model, rope_conv(range.map_unit(ByteOffset), rope).ok()); + return self.index.hover_property_name( + method, + model, + Some(rope_conv(range.map_unit(ByteOffset), rope)), + ); } } @@ -235,12 +229,8 @@ impl Backend { ast: Tree, rope: RopeSlice<'_>, ) -> anyhow::Result> { - let uri = ¶ms.text_document_position.text_document.uri; let position = params.text_document_position.position; - let Ok(ByteOffset(offset)) = rope_conv(position, rope) else { - return Err(errloc!("could not find offset for {}", uri.path().as_str())); - }; - + let ByteOffset(offset) = rope_conv(position, rope); let path = some!(params.text_document_position.text_document.uri.to_file_path()); let completions_limit = self .workspaces @@ -276,10 +266,7 @@ impl Backend { // Extract the current prefix (excluding quotes) let inner_range = range.shrink(1); let prefix = &contents[inner_range.start..offset]; - let lsp_range = ok!( - rope_conv(inner_range.map_unit(ByteOffset), rope), - "range conversion failed" - ); + let lsp_range = rope_conv(inner_range.map_unit(ByteOffset), rope); let mut items = MaxVec::new(completions_limit); self.index.complete_model(prefix, lsp_range, &mut items)?; diff --git a/src/model.rs b/src/model.rs index 030e970..b772787 100644 --- a/src/model.rs +++ b/src/model.rs @@ -7,6 +7,7 @@ use std::fmt::Display; use std::ops::Deref; use std::sync::Arc; use std::sync::RwLock; +use std::sync::atomic::AtomicBool; use dashmap::DashMap; use dashmap::mapref::one::RefMut; @@ -86,6 +87,12 @@ pub enum PropertyKind { Method, } +/// Twin of [PropertyKind] where field contains field type info +pub enum PropertyInfo { + Field(Spur), + Method, +} + #[derive(Clone, Debug)] pub struct Field { pub kind: FieldKind, @@ -94,12 +101,23 @@ pub struct Field { pub help: Option, } -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct Method { - pub return_type: MethodReturnType, pub locations: Vec, pub docstring: Option, pub arguments: Option>, + pub pending_eval: AtomicBool, +} + +impl Clone for Method { + fn clone(&self) -> Self { + Self { + locations: self.locations.clone(), + docstring: self.docstring.clone(), + arguments: self.arguments.clone(), + pending_eval: AtomicBool::new(false), + } + } } #[derive(Deref, DerefMut, Clone, Debug)] @@ -117,16 +135,6 @@ impl From for TrackedMinLoc { } } -#[derive(Clone, Debug, Default)] -pub enum MethodReturnType { - #[default] - Unprocessed, - /// Set to prevent recursion - Processing, - Value, - Relational(Symbol), -} - impl Field { pub fn merge<'this>(self: &'this mut Arc, other: &Self) -> &'this mut Self { let self_ = Arc::make_mut(self); @@ -423,7 +431,9 @@ impl ModelIndex { locations_filter: &[PathSymbol], ) -> Option> { let model_name = _R(model); - let mut entry = self.try_get_mut(&model).expect(format_loc!("deadlock"))?; + let Some(mut entry) = self.try_get_mut(&model).try_unwrap() else { + panic!("{} deadlock on model {}", loc!(), _R(model)); + }; if entry.fields.is_some() && entry.methods.is_some() && locations_filter.is_empty() { return Some(entry); } @@ -610,7 +620,6 @@ impl ModelIndex { { loc.active = false; method.arguments = None; - method.return_type = MethodReturnType::Unprocessed; } } } @@ -675,10 +684,10 @@ impl ModelIndex { Entry::Vacant(empty) => { empty.insert( Method { - return_type: Default::default(), locations: vec![method_location.into()], docstring: None, arguments: None, + pending_eval: AtomicBool::new(false), } .into(), ); @@ -852,19 +861,15 @@ impl ModelEntry { Ok(()) } - pub fn prop_kind(&self, prop: Spur) -> Option { - if self - .fields - .as_ref() - .is_some_and(|fields| fields.contains_key(&prop.into())) - { - Some(PropertyKind::Field) + pub fn prop_kind(&self, prop: Spur) -> Option { + if let Some(field) = self.fields.as_ref().and_then(|fields| fields.get(&prop.into())) { + Some(PropertyInfo::Field(field.type_)) } else if self .methods .as_ref() .is_some_and(|methods| methods.contains_key(&prop.into())) { - Some(PropertyKind::Method) + Some(PropertyInfo::Method) } else { None } diff --git a/src/python.rs b/src/python.rs index f08daf1..37796ac 100644 --- a/src/python.rs +++ b/src/python.rs @@ -563,9 +563,7 @@ impl Backend { .ast_map .get(file_path_str) .ok_or_else(|| errloc!("Did not build AST for {}", file_path_str))?; - let Ok(ByteOffset(offset)) = rope_conv(params.text_document_position_params.position, rope) else { - return Err(errloc!("could not find offset for {}", file_path_str)); - }; + let ByteOffset(offset) = rope_conv(params.text_document_position_params.position, rope); let contents = Cow::from(rope); let root = some!(top_level_stmt(ast.root_node(), offset)); @@ -837,7 +835,7 @@ impl Backend { params: ReferenceParams, rope: RopeSlice<'_>, ) -> anyhow::Result>> { - let ByteOffset(offset) = ok!(rope_conv(params.text_document_position.position, rope)); + let ByteOffset(offset) = rope_conv(params.text_document_position.position, rope); let uri = ¶ms.text_document_position.text_document.uri; let file_path = uri.to_file_path().unwrap(); let file_path_str = file_path.to_str().unwrap(); @@ -948,9 +946,7 @@ impl Backend { .ast_map .get(file_path_str) .ok_or_else(|| errloc!("Did not build AST for {}", file_path_str))?; - let Ok(ByteOffset(offset)) = rope_conv(params.text_document_position_params.position, rope) else { - return Err(errloc!("could not find offset for {}", file_path_str)); - }; + let ByteOffset(offset) = rope_conv(params.text_document_position_params.position, rope); let contents = Cow::from(rope); let root = some!(top_level_stmt(ast.root_node(), offset)); @@ -970,7 +966,13 @@ impl Backend { let slice = ok!(rope.try_slice(range.clone())); let slice = Cow::from(slice); return self.index.hover_model(&slice, Some(lsp_range), false, None); - } else if range.end < offset { + } + if range.end < offset + && match_ + .nodes_for_capture_index(PyCompletions::Prop as _) + .next() + .is_some() + { this_model.tag_model(capture.node, match_, root.byte_range(), &contents); } } @@ -998,9 +1000,7 @@ impl Backend { ); } let model = _R(model); - return self - .index - .hover_property_name(needle, model, rope_conv(range, rope).ok()); + return (self.index).hover_property_name(needle, model, Some(rope_conv(range, rope))); } else if let Some(cmdlist) = capture.node.next_named_sibling() && Backend::is_commandlist(cmdlist, offset) { @@ -1014,7 +1014,7 @@ impl Backend { &contents, false, )); - let range = rope_conv(range, rope).ok(); + let range = Some(rope_conv(range, rope)); return self.index.hover_property_name(needle, _R(model), range); } } @@ -1033,9 +1033,7 @@ impl Backend { slice = needle; } } - return self - .index - .hover_record(slice, rope_conv(range.map_unit(ByteOffset), rope).ok()); + return (self.index).hover_record(slice, Some(rope_conv(range.map_unit(ByteOffset), rope))); } Some(PyCompletions::Prop) if range.contains(&offset) => { let model = some!(this_model.inner); @@ -1082,9 +1080,7 @@ impl Backend { ); } let model = _R(model); - return self - .index - .hover_property_name(needle, model, rope_conv(range, rope).ok()); + return (self.index).hover_property_name(needle, model, Some(rope_conv(range, rope))); } else if matches!(descriptor, "groups") { let range = desc_value.byte_range().shrink(1); let value = Cow::from(ok!(rope.try_slice(range.clone()))); @@ -1093,7 +1089,7 @@ impl Backend { let (needle, byte_range) = some!(ref_); return self .index - .hover_record(needle, rope_conv(byte_range.map_unit(ByteOffset), rope).ok()); + .hover_record(needle, Some(rope_conv(byte_range.map_unit(ByteOffset), rope))); } return Ok(None); @@ -1113,7 +1109,7 @@ impl Backend { } } if let Some((model, prop, range)) = self.attribute_at_offset(offset, root, &contents) { - let lsp_range = rope_conv(range.map_unit(ByteOffset), rope).ok(); + let lsp_range = Some(rope_conv(range.map_unit(ByteOffset), rope)); return self.index.hover_property_name(prop, model, lsp_range); } @@ -1129,19 +1125,18 @@ impl Backend { return self.index.hover_model(model, Some(lsp_range), true, identifier); } - // not a model! we only have so many things we can hover... - match type_ { - Type::Method(model, method) => self.index.hover_property_name(&method, _R(model), Some(lsp_range)), - _ => Ok(None), - } + self.index.hover_variable( + (needle.kind() == "identifier").then(|| &contents[needle.byte_range()]), + type_, + Some(lsp_range), + ) } pub(crate) fn python_signature_help(&self, params: SignatureHelpParams) -> anyhow::Result> { use std::fmt::Write; let uri = ¶ms.text_document_position_params.text_document.uri; - let document = - some!((self.document_map).get(uri.path().as_str())); + let document = some!((self.document_map).get(uri.path().as_str())); let file_path = uri.to_file_path().unwrap(); let ast = some!((self.ast_map).get(file_path.to_str().unwrap())); let contents = Cow::from(&document.rope); @@ -1164,9 +1159,8 @@ impl Backend { } let active_parameter = 'find_param: { - if let Ok(offset) = - rope_conv::<_, ByteOffset>(params.text_document_position_params.position, document.rope.slice(..)) - && let Some(contents) = contents.get(..=offset.0) + let ByteOffset(offset) = rope_conv(params.text_document_position_params.position, document.rope.slice(..)); + if let Some(contents) = contents.get(..=offset) && let Some(idx) = contents.bytes().rposition(|c| c == b',' || c == b'(') { if contents.as_bytes()[idx] == b'(' { @@ -1192,7 +1186,7 @@ impl Backend { return Ok(None); }; let method_key = some!(_G(&method)); - let rtype = (self.index).resolve_method_returntype(method_key.into(), model_key.into()); + let rtype = (self.index).eval_method_rtype(method_key.into(), model_key.into(), None); let model = some!((self.index).models.get(&model_key)); let method_obj = some!(some!(model.methods.as_ref()).get(&method_key.into())); @@ -1215,8 +1209,9 @@ impl Backend { }); } + let rtype = rtype.as_ref().and_then(|rtype| self.index.type_display(rtype)); match rtype { - Some(rtype) => drop(write!(&mut label, ") -> Model[\"{}\"]", _R(rtype))), + Some(rtype) => drop(write!(&mut label, ") -> {rtype}")), None => label.push_str(") -> ..."), }; @@ -1250,7 +1245,7 @@ impl Backend { let ast = some!(self.ast_map.get(file_path.to_str().unwrap())); let rope = &document.rope; let contents = Cow::from(rope); - let ByteOffset(offset) = some!(rope_conv(params.position, rope.slice(..)).ok()); + let ByteOffset(offset) = rope_conv(params.position, rope.slice(..)); let root = some!(top_level_stmt(ast.root_node(), offset)); let needle = some!(root.named_descendant_for_byte_range(offset, offset)); let (type_, _) = some!((self.index).type_of_range(root, needle.byte_range().map_unit(ByteOffset), &contents)); diff --git a/src/python/completions.rs b/src/python/completions.rs index 11dda06..0d0a82b 100644 --- a/src/python/completions.rs +++ b/src/python/completions.rs @@ -5,7 +5,7 @@ use std::borrow::Cow; use std::sync::atomic::Ordering::Relaxed; use tower_lsp_server::{UriExt, lsp_types::*}; -use tracing::{debug, warn}; +use tracing::debug; use tree_sitter::Tree; use crate::prelude::*; @@ -28,10 +28,7 @@ impl Backend { ast: Tree, rope: RopeSlice<'_>, ) -> anyhow::Result> { - let Ok(ByteOffset(offset)) = rope_conv(params.text_document_position.position, rope) else { - warn!("invalid position {:?}", params.text_document_position.position); - return Ok(None); - }; + let ByteOffset(offset) = rope_conv(params.text_document_position.position, rope); let path = some!(params.text_document_position.text_document.uri.to_file_path()); let Some(current_module) = self.index.find_module_of(&path) else { debug!("no current module"); @@ -106,7 +103,7 @@ impl Backend { Some(PyCompletions::Model) => { if range.contains_end(offset) { let (needle, byte_range) = extract_string_needle_at_offset(rope, range, offset)?; - let range = ok!(rope_conv(byte_range, rope)); + let range = rope_conv(byte_range, rope); early_return.lift(move || async move { let mut items = MaxVec::new(completions_limit); self.index.complete_model(&needle, range, &mut items)?; @@ -333,7 +330,7 @@ impl Backend { let range = desc_value.byte_range(); let (needle, byte_range) = extract_string_needle_at_offset(rope, range, offset)?; - let range = ok!(rope_conv(byte_range, rope)); + let range = rope_conv(byte_range, rope); early_return.lift(move || async move { let mut items = MaxVec::new(completions_limit); self.index.complete_model(&needle, range, &mut items)?; diff --git a/src/python/diagnostics.rs b/src/python/diagnostics.rs index b31b45a..5c6419f 100644 --- a/src/python/diagnostics.rs +++ b/src/python/diagnostics.rs @@ -37,10 +37,8 @@ impl Backend { let before_count = diagnostics.len(); diagnostics.retain(|diag| { // If we couldn't get a range here, rope has changed significantly so just toss the diag. - let Ok(range) = rope_conv::<_, ByteRange>(diag.range, rope) else { - return false; - }; - !root.byte_range().contains(&range.start.0) + let ByteOffset(start) = rope_conv(diag.range.start, rope); + !root.byte_range().contains(&start) }); debug!( "Retained {}/{} diagnostics after damage zone check", @@ -107,8 +105,7 @@ impl Backend { } if !id_found { - let range = rope_conv(range.map_unit(ByteOffset), rope) - .expect(format_loc!("failed to get range for xmlid diag")); + let range = rope_conv(range.map_unit(ByteOffset), rope); diagnostics.push(Diagnostic { range, message: format!("No XML record with ID `{xmlid}` found"), @@ -128,7 +125,7 @@ impl Backend { let has_model = model_key.map(|model| self.index.models.contains_key(&model.into())); if !has_model.unwrap_or(false) { diagnostics.push(Diagnostic { - range: rope_conv(range.map_unit(ByteOffset), rope).unwrap(), + range: rope_conv(range.map_unit(ByteOffset), rope), message: format!("`{model}` is not a valid model name"), severity: Some(DiagnosticSeverity::ERROR), ..Default::default() @@ -151,7 +148,7 @@ impl Backend { let has_model = model_key.map(|model| self.index.models.contains_key(&model.into())); if !has_model.unwrap_or(false) { diagnostics.push(Diagnostic { - range: rope_conv(range.map_unit(ByteOffset), rope).unwrap(), + range: rope_conv(range.map_unit(ByteOffset), rope), message: format!("`{model}` is not a valid model name"), severity: Some(DiagnosticSeverity::ERROR), ..Default::default() @@ -239,7 +236,7 @@ impl Backend { let has_model = model_key.map(|model| self.index.models.contains_key(&model.into())); if !has_model.unwrap_or(false) { diagnostics.push(Diagnostic { - range: rope_conv(range.map_unit(ByteOffset), rope).unwrap(), + range: rope_conv(range.map_unit(ByteOffset), rope), message: format!("`{model}` is not a valid model name"), severity: Some(DiagnosticSeverity::ERROR), ..Default::default() @@ -484,7 +481,7 @@ impl Backend { if let Some(dot) = needle.find('.') { let message_range = range.start.0 + dot..range.end.0; diagnostics.push(Diagnostic { - range: rope_conv(message_range.map_unit(ByteOffset), rope).unwrap(), + range: rope_conv(message_range.map_unit(ByteOffset), rope), severity: Some(DiagnosticSeverity::ERROR), message: "Dotted access is not supported in this context".to_string(), ..Default::default() @@ -497,7 +494,7 @@ impl Backend { Ok(()) => {} Err(ResolveMappedError::NonRelational) => { diagnostics.push(Diagnostic { - range: rope_conv(range, rope).unwrap(), + range: rope_conv(range, rope), severity: Some(DiagnosticSeverity::ERROR), message: format!("`{needle}` is not a relational field"), ..Default::default() @@ -538,7 +535,7 @@ impl Backend { } if !has_property { diagnostics.push(Diagnostic { - range: rope_conv(range, rope).unwrap(), + range: rope_conv(range, rope), severity: Some(DiagnosticSeverity::ERROR), message: format!( "Model `{}` has no {} `{needle}`", diff --git a/src/python/tests.rs b/src/python/tests.rs index db6151b..0f97943 100644 --- a/src/python/tests.rs +++ b/src/python/tests.rs @@ -390,10 +390,10 @@ class TestModel(models.Model): }; // Check if followed by colon - if let Some(next) = string_node.next_sibling() { - if next.kind() == ":" { - has_proper_syntax = true; - } + if let Some(next) = string_node.next_sibling() + && next.kind() == ":" + { + has_proper_syntax = true; } } diff --git a/src/record.rs b/src/record.rs index f9861f9..8ff670a 100644 --- a/src/record.rs +++ b/src/record.rs @@ -37,7 +37,7 @@ impl Record { // nested records are a thing apparently let mut stack = 1; let mut in_record = true; - let start: Position = ok!(rope_conv(offset, rope), "{}", path); + let start: Position = rope_conv(offset, rope); loop { match reader.next() { @@ -97,7 +97,7 @@ impl Record { line: err.pos().row - 1, character: err.pos().col - 1, }; - end = rope_conv(pos, rope).ok(); + end = Some(rope_conv(pos, rope)); break; } _ => {} @@ -105,7 +105,7 @@ impl Record { } let id = some!(id); let end = end.ok_or_else(|| errloc!("Unbound range for record"))?; - let end = ok!(rope_conv(end, rope), "{}", path); + let end = rope_conv(end, rope); let range = Range { start, end }; Ok(Some(Self { @@ -124,7 +124,7 @@ impl Record { reader: &mut Tokenizer, rope: RopeSlice<'_>, ) -> anyhow::Result> { - let start: Position = ok!(rope_conv(offset, rope), "{}", path); + let start: Position = rope_conv(offset, rope); let mut id = None; let mut inherit_id = None; let mut end = None; @@ -190,14 +190,14 @@ impl Record { line: err.pos().row - 1, character: err.pos().col - 1, }; - end = rope_conv(pos, rope).ok(); + end = Some(rope_conv(pos, rope)); break; } _ => {} } } let end = end.ok_or_else(|| errloc!("Unbound range for template"))?; - let end = ok!(rope_conv(end, rope), "{}", path); + let end = rope_conv(end, rope); let range = Range { start, end }; Ok(Some(Self { @@ -218,7 +218,7 @@ impl Record { ) -> anyhow::Result> { let mut id = None; let mut end = None; - let start = ok!(rope_conv(offset, rope), "{}", path); + let start = rope_conv(offset, rope); loop { match reader.next() { @@ -239,7 +239,7 @@ impl Record { line: err.pos().row - 1, character: err.pos().col - 1, }; - end = rope_conv(pos, rope).ok(); + end = Some(rope_conv(pos, rope)); break; } _ => {} @@ -248,7 +248,7 @@ impl Record { let id = some!(id); let end = end.ok_or_else(|| errloc!("Unbound range for menuitem"))?; - let end = ok!(rope_conv(end, rope), "{}", path); + let end = rope_conv(end, rope); let range = Range { start, end }; Ok(Some(Self { diff --git a/src/server.rs b/src/server.rs index 00f64d5..ebfd938 100644 --- a/src/server.rs +++ b/src/server.rs @@ -124,7 +124,7 @@ impl LanguageServer for Backend { self.document_map.remove(path); self.record_ranges.remove(path); - + let file_path = params.text_document.uri.to_file_path().unwrap(); self.ast_map.remove(file_path.to_str().unwrap()); @@ -279,8 +279,7 @@ impl LanguageServer for Backend { document.rope = ropey::Rope::from_str(&change.text); } else { let range = change.range.expect("LSP change event must have a range"); - let range: CharRange = - rope_conv(range, document.rope.slice(..)).expect("did_change applying delta: no range"); + let range: CharRange = rope_conv(range, document.rope.slice(..)); let rope = &mut document.rope; rope.remove(range.erase()); if !change.text.is_empty() { diff --git a/src/template.rs b/src/template.rs index b3db882..932ed5e 100644 --- a/src/template.rs +++ b/src/template.rs @@ -80,14 +80,8 @@ pub fn gather_templates( } }; let name = _I(name).into(); - let start = ok!( - rope_conv(ByteOffset(tag_start), document), - "qweb_templates start <- tag_start" - ); - let end = ok!( - rope_conv(ByteOffset(span.end()), document), - "qweb_templates end <- span.end()" - ); + let start = rope_conv(ByteOffset(tag_start), document); + let end = rope_conv(ByteOffset(span.end()), document); let range = Range { start, end }; templates.push(NewTemplate { base, @@ -122,10 +116,7 @@ pub fn gather_templates( let name_candidate = if base { t_name } else { t_inherit }; let Some(name) = name_candidate else { break }; let name = _I(name).into(); - let start = ok!( - rope_conv(ByteOffset(tag_start), document), - "qweb_templates start <- tag_start" - ); + let start = rope_conv(ByteOffset(tag_start), document); let end = Position { line: err.pos().row, character: err.pos().col, diff --git a/src/utils.rs b/src/utils.rs index fd98717..193593c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -168,69 +168,64 @@ where /// - [Range] -> [CharRange] /// - [Range] <-> [ByteRange] #[inline] -pub fn rope_conv(src: T, rope: RopeSlice<'_>) -> Result>>::Error> +pub fn rope_conv(src: T, rope: RopeSlice<'_>) -> U where - for<'a> U: TryFrom>, + for<'a> U: From>, { - RopeAdapter(src, rope).try_into() + RopeAdapter(src, rope).into() } -impl<'a> TryFrom> for Position { - type Error = std::convert::Infallible; - fn try_from(value: RopeAdapter<'a, ByteOffset>) -> Result { +impl<'a> From> for Position { + fn from(value: RopeAdapter<'a, ByteOffset>) -> Self { let RopeAdapter(offset, rope) = value; let line = rope.byte_to_line_idx(offset.0, LINE_TYPE); let line_start_byte = rope.line_to_byte_idx(line, LINE_TYPE); let line_start_char = rope.byte_to_char_idx(line_start_byte); let char_offset = rope.byte_to_char_idx(offset.0); let column = char_offset - line_start_char; - Ok(Position::new(line as u32, column as u32)) + Position::new(line as u32, column as u32) } } -impl<'a> TryFrom> for ByteOffset { - type Error = ropey::Error; - fn try_from(value: RopeAdapter<'a, Position>) -> Result { +impl<'a> From> for ByteOffset { + fn from(value: RopeAdapter<'a, Position>) -> Self { let RopeAdapter(position, rope) = value; - let CharOffset(char_offset) = position_to_char(position, rope)?; + let CharOffset(char_offset) = position_to_char(position, rope); let byte_offset = rope.char_to_byte_idx(char_offset); - Ok(ByteOffset(byte_offset)) + ByteOffset(byte_offset) } } -impl<'a> TryFrom> for CharRange { - type Error = ropey::Error; - fn try_from(value: RopeAdapter<'a, Range>) -> Result { +impl<'a> From> for CharRange { + fn from(value: RopeAdapter<'a, Range>) -> Self { let RopeAdapter(range, rope) = value; - let start = position_to_char(range.start, rope)?; - let end = position_to_char(range.end, rope)?; - Ok(start..end) + let start = position_to_char(range.start, rope); + let end = position_to_char(range.end, rope); + start..end } } -impl<'a> TryFrom> for ByteRange { - type Error = ropey::Error; - fn try_from(value: RopeAdapter<'a, Range>) -> Result { +impl<'a> From> for ByteRange { + fn from(value: RopeAdapter<'a, Range>) -> Self { let RopeAdapter(range, rope) = value; - let start = rope_conv(range.start, rope)?; - let end = rope_conv(range.end, rope)?; - Ok(start..end) + let start = rope_conv(range.start, rope); + let end = rope_conv(range.end, rope); + start..end } } -impl<'a> TryFrom> for Range { - type Error = std::convert::Infallible; - fn try_from(value: RopeAdapter<'a, ByteRange>) -> Result { +impl<'a> From> for Range { + fn from(value: RopeAdapter<'a, ByteRange>) -> Self { let RopeAdapter(range, rope) = value; - let start = rope_conv(range.start, rope)?; - let end = rope_conv(range.end, rope)?; - Ok(Range { start, end }) + let start = rope_conv(range.start, rope); + let end = rope_conv(range.end, rope); + Range { start, end } } } -fn position_to_char(position: Position, rope: RopeSlice<'_>) -> ropey::Result { +fn position_to_char(position: Position, rope: RopeSlice<'_>) -> CharOffset { let line_offset_in_byte = rope.line_to_byte_idx(position.line as usize, LINE_TYPE); let line_offset_in_char = rope.byte_to_char_idx(line_offset_in_byte); - Ok(CharOffset(line_offset_in_char + position.character as usize)) + CharOffset(line_offset_in_char + position.character as usize) } impl From> for Position { @@ -622,6 +617,20 @@ pub fn to_display_path(path: impl AsRef) -> String { path.as_ref().to_string_lossy().into_owned() } +pub struct Defer(pub Option) +where + T: FnOnce(); + +impl Drop for Defer +where + T: FnOnce(), +{ + fn drop(&mut self) { + let func = self.0.take().unwrap(); + func() + } +} + /// On Windows, rewrites the wide path prefix `\\?\C:` to `C:` /// Source: https://stackoverflow.com/a/70970317 #[inline] diff --git a/src/utils/lru.rs b/src/utils/lru.rs new file mode 100644 index 0000000..845e91a --- /dev/null +++ b/src/utils/lru.rs @@ -0,0 +1,52 @@ +use std::collections::{HashMap, VecDeque}; + +pub struct LruCache { + map: HashMap, + order: VecDeque, + capacity: usize, +} + +impl Default for LruCache +where + K: Clone + Eq + std::hash::Hash, +{ + #[inline] + fn default() -> Self { + Self::new(16) + } +} + +impl LruCache { + pub fn new(capacity: usize) -> Self { + Self { + map: Default::default(), + order: Default::default(), + capacity, + } + } + + pub fn get(&mut self, key: &K) -> Option<&V> { + let item = self.map.get(key); + if item.is_some() { + // let mut order = self.order.write(); + // move key to the back (most recent) + self.order.retain(|k| k != key); + self.order.push_back(key.clone()); + } + item + } + + pub fn put(&mut self, key: K, value: V) { + if self.map.contains_key(&key) { + // update existing, move key + self.order.retain(|k| k != &key); + } else if self.map.len() == self.capacity { + // evict least recent + if let Some(old) = self.order.pop_front() { + self.map.remove(&old); + } + } + self.map.insert(key.clone(), value); + self.order.push_back(key); + } +} diff --git a/src/xml.rs b/src/xml.rs index 393b1f0..36e3c3d 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -129,7 +129,7 @@ impl Backend { record_ranges.extend( entries .into_iter() - .map(|entry| rope_conv(entry.template.location.unwrap().range, rope).unwrap()), + .map(|entry| rope_conv(entry.template.location.unwrap().range, rope)), ); continue; } @@ -142,10 +142,7 @@ impl Backend { continue; } }; - let Ok(range) = rope_conv(record.location.range, rope) else { - debug!("no range for {}", record.id); - continue; - }; + let range = rope_conv(record.location.range, rope); record_ranges.push(range); if let Some(prefix) = record_prefix.as_mut() { self.index @@ -161,7 +158,7 @@ impl Backend { record_ranges.extend( entries .into_iter() - .map(|entry| rope_conv(entry.template.location.unwrap().range, rope).unwrap()), + .map(|entry| rope_conv(entry.template.location.unwrap().range, rope)), ); } } @@ -191,7 +188,7 @@ impl Backend { .record_ranges .get(uri.path().as_str()) .ok_or_else(|| errloc!("Did not build record ranges for {}", uri.path().as_str()))?; - let mut offset_at_cursor = ok!(rope_conv(position, rope)); + let mut offset_at_cursor = rope_conv(position, rope); let Ok(record) = ranges.value().binary_search_by(|range| { if offset_at_cursor < range.start { Ordering::Greater @@ -374,11 +371,10 @@ impl Backend { let (mut needle, ref_range) = some!(ref_at_cursor); - let mut lsp_range = rope_conv( + let mut lsp_range = Some(rope_conv( ref_range.clone().map_unit(|unit| ByteOffset(unit + relative_offset)), rope, - ) - .ok(); + )); let current_module = self.index.find_module_of(&path); match ref_kind { Some(RefKind::Model) => self.index.hover_model(needle, lsp_range, false, None), @@ -427,14 +423,13 @@ impl Backend { let cursor_node = some!(ast.root_node().named_descendant_for_byte_range(py_offset, py_offset)); let needle = &needle[cursor_node.byte_range()]; let scope_type = some!(scope.get(needle)); - let lsp_range = rope_conv( + let lsp_range = Some(rope_conv( cursor_node .byte_range() .clone() .map_unit(|unit| ByteOffset(unit + ref_range.start + relative_offset)), rope, - ) - .ok(); + )); if let Some(model) = (self.index).try_resolve_model(scope_type, &scope) { return self.index.hover_model(_R(model), lsp_range, true, Some(needle)); } @@ -452,7 +447,10 @@ impl Backend { self.index.hover_property_name( field, _R(model), - rope_conv(range.map_unit(|rel_unit| ByteOffset(rel_unit + anchor)), rope).ok(), + Some(rope_conv( + range.map_unit(|rel_unit| ByteOffset(rel_unit + anchor)), + rope, + )), ) } Some(RefKind::Component) => Ok(self.index.hover_component(needle, lsp_range)), @@ -604,7 +602,7 @@ impl Index { )? } RefKind::Model => { - let range = rope_conv(replace_range, rope)?; + let range = rope_conv(replace_range, rope); self.complete_model(needle, range, &mut items)? } ref ref_kind @ RefKind::PropertyName(ref access) | ref ref_kind @ RefKind::MethodName(ref access) => { @@ -637,7 +635,7 @@ impl Index { )?; } RefKind::TInherit | RefKind::TCall => { - let range = rope_conv(replace_range, rope)?; + let range = rope_conv(replace_range, rope); self.complete_template_name(needle, range, &mut items)?; } RefKind::PropOf(component) => { @@ -1098,8 +1096,8 @@ impl Index { // // move one place back to get the attribute name expected_eq_pos.col = expected_eq_pos.col.saturating_sub(1); - let start_pos: ByteOffset = rope_conv(span_conv(start_pos), slice)?; - let expected_eq_pos: ByteOffset = rope_conv(span_conv(expected_eq_pos), slice)?; + let start_pos: ByteOffset = rope_conv(span_conv(start_pos), slice); + let expected_eq_pos: ByteOffset = rope_conv(span_conv(expected_eq_pos), slice); // may be an invalid attribute, but no point in checking let mut range = (start_pos..expected_eq_pos).erase(); diff --git a/testing/Cargo.toml b/testing/Cargo.toml index 19eafe6..5d42e98 100644 --- a/testing/Cargo.toml +++ b/testing/Cargo.toml @@ -27,10 +27,10 @@ tracing-subscriber.workspace = true odoo-lsp.path = ".." [target.'cfg(target_os = "linux")'.dependencies] -iai-callgrind = { version = "0.16.0", features = ["client_requests"] } +iai-callgrind = { version = "0.16.1", features = ["client_requests"] } [target.'cfg(not(target_os = "linux"))'.dependencies] -iai-callgrind = "0.16.0" +iai-callgrind = "0.16.1" [[bench]] name = "standard" diff --git a/testing/fixtures/python_types/.odoo_lsp b/testing/fixtures/python_types/.odoo_lsp new file mode 100644 index 0000000..6c17534 --- /dev/null +++ b/testing/fixtures/python_types/.odoo_lsp @@ -0,0 +1 @@ +{"module":{"roots":["."]}} \ No newline at end of file diff --git a/testing/fixtures/python_types/bakery/__manifest__.py b/testing/fixtures/python_types/bakery/__manifest__.py new file mode 100644 index 0000000..980dd3b --- /dev/null +++ b/testing/fixtures/python_types/bakery/__manifest__.py @@ -0,0 +1 @@ +{"name": "bakery"} \ No newline at end of file diff --git a/testing/fixtures/python_types/bakery/models/models.py b/testing/fixtures/python_types/bakery/models/models.py new file mode 100644 index 0000000..908af4e --- /dev/null +++ b/testing/fixtures/python_types/bakery/models/models.py @@ -0,0 +1,70 @@ +from odoo import models, fields + + +class Bread(models.Model): + _name = 'bakery.bread' + + def _test(self): + items = {item: item for item in self} + #^type Dict([Model("bakery.bread"), Model("bakery.bread")]) + + foobar = {'a': self, 'b': 123} + #^type DictBag([("a", Model("bakery.bread")), ("b", PyBuiltin("int"))]) + + aaaa = foobar['a'] + #^type Model("bakery.bread") + + bbbb = foobar['b'] + #^type PyBuiltin("int") + + return foobar + + def identity(self, what): + return {'c': what} + + def _test_return(self): + foobar = self._test() + aaaa = foobar['a'] + #^type Model("bakery.bread") + bbbb = foobar['b'] + #^type PyBuiltin("int") + + baz = self.identity(self) + cccc = baz['c'] + #^type Model("bakery.bread") + + def test_variable_append(self): + foo = [] + #^type List(...) + foo.append(self) + foo + #^type List(Model("bakery.bread")) + + def test_dictkey_append(self): + foo = self.identity([]) + foo['c'].append(self) + cccc = foo['c'] + #^type List(Model("bakery.bread")) + elem = cccc[12] + #^type Model("bakery.bread") + + def test_dict_set(self): + foobar = {} + foobar['a'] = self + aaaa = foobar['a'] + #^type Model("bakery.bread") + foobar['b'] = nonexistent + foobar + #^type DictBag([("a", Model("bakery.bread")), ("b", Value)]) + + def test_dict_update(self): + foobar = {} + foobar.update({'a': self}) + aaaa = foobar['a'] + #^type Model("bakery.bread") + + def test_sanity(self): + foobar = ['what'] + #^type List(PyBuiltin("str")) + self._fields + #^type Dict([PyBuiltin("str"), Model("ir.model.fields")]) diff --git a/testing/src/tests.rs b/testing/src/tests.rs index b21553b..bbe1dd6 100644 --- a/testing/src/tests.rs +++ b/testing/src/tests.rs @@ -507,7 +507,7 @@ fn gather_expected(root: &Path, lang: TestLanguages) -> HashMap Date: Mon, 10 Nov 2025 17:21:11 -0500 Subject: [PATCH 02/15] iterables --- .clippy.toml | 7 + .vscode/launch.json | 164 ++++++++++++++- .vscode/settings.json | 5 +- src/analyze.rs | 192 ++++++++++++++++-- src/backend.rs | 2 +- src/index.rs | 6 +- src/index/js.rs | 2 + src/model.rs | 3 +- src/python.rs | 12 +- src/python/completions.rs | 4 +- src/python/diagnostics.rs | 2 +- src/utils.rs | 93 ++++++++- src/utils/lru.rs | 52 ----- .../python_types/bakery/models/models.py | 40 ++++ 14 files changed, 483 insertions(+), 101 deletions(-) delete mode 100644 src/utils/lru.rs diff --git a/.clippy.toml b/.clippy.toml index 1f4f342..40fecfc 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -1,5 +1,12 @@ cognitive-complexity-threshold = 20 +[[disallowed-methods]] +path = "tree_sitter::Node::named_child" + +[[disallowed-methods]] +path = "tree_sitter::Node::next_named_sibling" +replacement = "odoo_lsp::utils::python_next_named_sibling" + [[await-holding-invalid-types]] path = "dashmap::mapref::one::RefMut" reason = "Would deadlock if same thread wants to acquire a reference" diff --git a/.vscode/launch.json b/.vscode/launch.json index 463cb4e..8aa2fda 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,8 +7,13 @@ "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", - "args": ["--extensionDevelopmentPath=${workspaceRoot}", "${workspaceRoot}/examples"], - "outFiles": ["${workspaceRoot}/dist/*.js"], + "args": [ + "--extensionDevelopmentPath=${workspaceRoot}", + "${workspaceRoot}/examples" + ], + "outFiles": [ + "${workspaceRoot}/dist/*.js" + ], "preLaunchTask": "watch-all", "env": { "SERVER_PATH": "${workspaceRoot}/target/debug/odoo-lsp", @@ -21,8 +26,13 @@ "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", - "args": ["--extensionDevelopmentPath=${workspaceRoot}", "${workspaceRoot}/examples"], - "outFiles": ["${workspaceRoot}/dist/*.js"], + "args": [ + "--extensionDevelopmentPath=${workspaceRoot}", + "${workspaceRoot}/examples" + ], + "outFiles": [ + "${workspaceRoot}/dist/*.js" + ], "preLaunchTask": { "type": "npm", "script": "watch" @@ -38,7 +48,9 @@ "name": "Attach", "program": "${workspaceFolder}/target/debug/odoo-lsp", // "pid": "${command:pickMyProcess}", - "sourceLanguages": ["rust"], + "sourceLanguages": [ + "rust" + ], "windows": { "program": "${workspaceFolder}/target/debug/odoo-lsp.exe" } @@ -53,13 +65,149 @@ "--extensionTestsPath=${workspaceRoot}/client/out/test/index", "${workspaceRoot}/client/testFixture" ], - "outFiles": ["${workspaceRoot}/client/out/test/**/*.js"] + "outFiles": [ + "${workspaceRoot}/client/out/test/**/*.js" + ] + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'odoo_lsp'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=odoo-lsp" + ], + "filter": { + "name": "odoo_lsp", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'odoo-lsp'", + "cargo": { + "args": [ + "build", + "--bin=odoo-lsp", + "--package=odoo-lsp" + ], + "filter": { + "name": "odoo-lsp", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'odoo-lsp'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=odoo-lsp", + "--package=odoo-lsp" + ], + "filter": { + "name": "odoo-lsp", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'odoo_lsp_tests'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=odoo-lsp-tests" + ], + "filter": { + "name": "odoo_lsp_tests", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug benchmark 'standard'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bench=standard", + "--package=odoo-lsp-tests" + ], + "filter": { + "name": "standard", + "kind": "bench" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'ts-indent'", + "cargo": { + "args": [ + "build", + "--bin=ts-indent", + "--package=ts-indent" + ], + "filter": { + "name": "ts-indent", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'ts-indent'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=ts-indent", + "--package=ts-indent" + ], + "filter": { + "name": "ts-indent", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" } ], "compounds": [ { "name": "Client + Server", - "configurations": ["Launch Client", "Attach"] + "configurations": [ + "Launch Client", + "Attach" + ] } ] -} +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 8670db2..f6bd9f7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,8 @@ }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts "typescript.tsc.autoDetect": "off", - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "lldb.displayFormat": "auto", + "lldb.dereferencePointers": true, + "lldb.consoleMode": "commands" } diff --git a/src/analyze.rs b/src/analyze.rs index 1e7fe3f..2227d35 100644 --- a/src/analyze.rs +++ b/src/analyze.rs @@ -19,7 +19,7 @@ use crate::{ index::{_G, _I, _R, Index, Symbol}, model::{Method, ModelName, PropertyInfo}, test_utils, - utils::{ByteOffset, ByteRange, Defer, PreTravel, RangeExt, TryResultExt, rope_conv}, + utils::{ByteOffset, ByteRange, Defer, PreTravel, RangeExt, TryResultExt, python_next_named_sibling, rope_conv}, }; use ts_macros::query; @@ -71,6 +71,8 @@ pub enum Type { /// Equivalent to Value, but may have a better semantic name PyBuiltin(ImStr), List(ListElement), + Tuple(Vec), + Iterable(Option>), /// Can never be resolved, useful for non-model bindings. Value, } @@ -90,6 +92,16 @@ impl Debug for ListElement { } } +impl From for Option { + #[inline] + fn from(value: ListElement) -> Self { + match value { + ListElement::Vacant => None, + ListElement::Occupied(inner) => Some(*inner), + } + } +} + #[derive(Clone, PartialEq, Eq)] pub enum DictKey { String(ImStr), @@ -265,7 +277,7 @@ impl Index { // (_ left right) let lhs = node.named_child(0).unwrap(); if lhs.kind() == "identifier" - && let rhs = lhs.next_named_sibling().expect(format_loc!("rhs")) + && let rhs = python_next_named_sibling(lhs).expect(format_loc!("rhs")) && let Some(type_) = self.type_of(rhs, scope, contents) { let lhs = &contents[lhs.byte_range()]; @@ -273,7 +285,7 @@ impl Index { } else if lhs.kind() == "subscript" && let Some(map) = dig!(lhs, identifier) && let Some(key) = dig!(lhs, string(1).string_content(1)) - && let Some(rhs) = lhs.next_named_sibling() + && let Some(rhs) = python_next_named_sibling(lhs) && let type_ = self.type_of(rhs, scope, contents) && let Some(Type::DictBag(properties)) = scope.variables.get_mut(&contents[map.byte_range()]) { @@ -293,12 +305,12 @@ impl Index { // (for_statement left right body) scope.enter(true); let lhs = node.named_child(0).unwrap(); - if lhs.kind() == "identifier" - && let rhs = lhs.next_named_sibling().expect(format_loc!("rhs")) + + if let Some(rhs) = python_next_named_sibling(lhs) && let Some(type_) = self.type_of(rhs, scope, contents) + && let Some(inner) = Self::type_of_iterable(type_) { - let lhs = &contents[lhs.byte_range()]; - scope.insert(lhs.to_string(), type_); + unpack_forloop_pattern(lhs, inner, scope, contents); } return ControlFlow::Continue(true); } @@ -322,15 +334,14 @@ impl Index { "list_comprehension" | "set_comprehension" | "dictionary_comprehension" | "generator_expression" if node.byte_range().contains(&offset) => { - // (_ body (for_in_clause left right)) + // (_ body: _ (for_in_clause left: _ right: _)) let for_in = node.named_child(1).unwrap(); - let lhs = for_in.named_child(0).unwrap(); - if lhs.kind() == "identifier" - && let rhs = lhs.next_named_sibling().expect(format_loc!("rhs")) + if let Some(lhs) = for_in.child_by_field_name("left") + && let Some(rhs) = for_in.child_by_field_name("right") && let Some(type_) = self.type_of(rhs, scope, contents) + && let Some(inner) = Self::type_of_iterable(type_) { - let lhs = &contents[lhs.byte_range()]; - scope.insert(lhs.to_string(), type_); + unpack_forloop_pattern(lhs, inner, scope, contents); } } "call" if node.byte_range().contains_end(offset) => { @@ -441,7 +452,7 @@ impl Index { // (call (identifier) ..) // (as_pattern_target (identifier)))))) if let Some(value) = dig!(node, with_clause.with_item.as_pattern.call) - && let Some(target) = value.next_named_sibling() + && let Some(target) = python_next_named_sibling(value) && target.kind() == "as_pattern_target" && let Some(alias) = dig!(target, identifier) && let Some(callee) = value.named_child(0) @@ -593,13 +604,14 @@ impl Index { let mut comprehension_scope; let mut pair_scope = scope; if let Some(for_in_clause) = dig!(node, for_in_clause(1)) - && let Some(scrutinee) = dig!(for_in_clause, identifier) // TODO: pattern_list for `for a, b, ...` + && let Some(scrutinee) = for_in_clause.child_by_field_name("left") && let Some(iteratee) = for_in_clause.child_by_field_name("right") && let Some(iter_ty) = self.type_of(iteratee, scope, contents) + && let Some(iter_ty) = Self::type_of_iterable(iter_ty) { // FIXME: How to prevent this clone? comprehension_scope = Scope::new(Some(scope.clone())); - comprehension_scope.insert(contents[scrutinee.byte_range()].to_string(), iter_ty); + unpack_forloop_pattern(scrutinee, iter_ty, &mut comprehension_scope, contents); pair_scope = &comprehension_scope; } let lhs = pair @@ -658,8 +670,53 @@ impl Index { _ => None, } } + fn type_of_iterable(type_: Type) -> Option { + match type_ { + Type::Model(_) => Some(type_), + Type::List(inner) => inner.into(), + Type::Iterable(inner) => inner.map(|inner| *inner), + // TODO: tuple -> union + _ => None, + } + } fn type_of_call_node(&self, call: Node<'_>, scope: &Scope, contents: &str) -> Option { let func = call.named_child(0)?; + if func.kind() == "identifier" { + match &contents[func.byte_range()] { + "zip" => { + let args = call.named_child(1)?; + let mut cursor = args.walk(); + let children = args.named_children(&mut cursor).map(|child| { + Self::type_of_iterable(self.type_of(child, scope, contents).unwrap_or(Type::Value)) + .unwrap_or(Type::Value) + }); + return Some(Type::Iterable(Some(Box::new(Type::Tuple(children.collect()))))); + } + "enumerate" => { + let arg = call.named_child(1)?.named_child(0); + let arg = arg + .and_then(|arg| self.type_of(arg, scope, contents)) + .unwrap_or(Type::Value); + return Some(Type::Iterable(Some(Box::new(Type::Tuple(vec![ + Type::PyBuiltin(ImStr::from_static("int")), + arg, + ]))))); + } + "tuple" => { + let args = call.named_child(1)?; + if args.kind() == "argument_list" { + let mut cursor = args.walk(); + let children = args + .named_children(&mut cursor) + .map(|child| self.type_of(child, scope, contents).unwrap_or(Type::Value)); + return Some(Type::Tuple(children.collect())); + } + } + "super" => {} + _ => return None, + }; + } + let func = self.type_of(func, scope, contents)?; match func { Type::RefFn => { @@ -699,6 +756,58 @@ impl Index { _ => None, } } + Type::Method(model, read_group) if read_group == "_read_group" => { + let mut groupby = vec![]; + let mut aggs = vec![]; + let args = call.named_child(1)?; + + fn gather_attributes<'out>(contents: &'out str, arg: Node, out: &mut Vec<&'out str>) { + let mut cursor = arg.walk(); + for field in arg.named_children(&mut cursor) { + if let Some(field) = dig!(field, string_content(1)) { + let mut field = &contents[field.byte_range()]; + if let Some((inner, _)) = field.split_once(':') { + field = inner; + } + out.push(field); + } + } + } + + for (idx, arg) in args.named_children(&mut args.walk()).enumerate().take(3) { + if arg.kind() == "keyword_argument" { + let out = match &contents[arg.child_by_field_name("key")?.byte_range()] { + "groupby" => &mut groupby, + "aggregates" => &mut aggs, + _ => continue, + }; + let Some(arg) = arg.child_by_field_name("value") else { + continue; + }; + if arg.kind() != "list" { + continue; + } + gather_attributes(contents, arg, out); + continue; + } + + if arg.kind() != "list" || idx > 2 || idx == 0 { + continue; + } + + let out = if idx == 1 { &mut groupby } else { &mut aggs }; + gather_attributes(contents, arg, out); + } + + groupby.extend(aggs); + groupby.dedup(); + let model = Type::Model(ImStr::from_static(_R(model))); + // FIXME: This is not quite correct as only recordset and numeric aggregations make sense. + let aggs = groupby + .into_iter() + .map(|attr| self.type_of_attribute(&model, attr, scope).unwrap_or(Type::Value)); + Some(Type::List(ListElement::Occupied(Box::new(Type::Tuple(aggs.collect()))))) + } Type::Method(model, method) => { let method = _G(&method)?; let args = self.prepare_call_scope(model, method.into(), call, scope, contents); @@ -712,9 +821,12 @@ impl Index { | Type::PyBuiltin(..) | Type::Dict(..) | Type::DictBag(..) - | Type::List(..) => None, + | Type::List(..) + | Type::Iterable(..) + | Type::Tuple(..) => None, } } + #[instrument(skip_all, fields(model, method))] fn prepare_call_scope( &self, @@ -771,14 +883,15 @@ impl Index { let lhs = attribute.named_child(0)?; let lhs = self.type_of(lhs, scope, contents)?; let rhs = attribute.named_child(1)?; + let attrname = &contents[rhs.byte_range()]; match &contents[rhs.byte_range()] { "env" if matches!(lhs, Type::Model(..) | Type::Record(..) | Type::HttpRequest) => Some(Type::Env), "ref" if matches!(lhs, Type::Env) => Some(Type::RefFn), "user" if matches!(lhs, Type::Env) => Some(Type::Model("res.users".into())), "company" | "companies" if matches!(lhs, Type::Env) => Some(Type::Model("res.company".into())), - "mapped" => { + "mapped" | "_read_group" => { let model = self.try_resolve_model(&lhs, scope)?; - Some(Type::Method(model, "mapped".into())) + Some(Type::Method(model, attrname.into())) } func if MODEL_METHODS.contains(func) => match lhs { Type::Model(model) => Some(Type::ModelFn(model)), @@ -917,6 +1030,21 @@ impl Index { let record = self.records.get(&xml_id.into())?; Some(_R(record.model?).into()) } + Type::Tuple(items) => Some( + fomat! { + "tuple[" + for item in items { + (self.type_display(item).as_deref().unwrap_or("...")) + } sep { ", " } + "]" + } + .into(), + ), + Type::Iterable(output) => { + let output = output.as_deref().and_then(|inner| self.type_display(inner)); + let output = output.as_deref().unwrap_or("..."); + Some(format!("Iterable[{output}]").into()) + } Type::Method(..) => unreachable!("Bug: this function should not handle methods"), Type::RefFn | Type::ModelFn(_) | Type::Super | Type::HttpRequest | Type::Value => None, } @@ -1152,6 +1280,32 @@ pub fn determine_scope<'out, 'node>( Some((self_type, fn_scope, self_param)) } +/// `pattern` is `(identifier | pattern_list | tuple_pattern)`, the `a, b` in `for a, b in ...`. +fn unpack_forloop_pattern(pattern: Node, type_: Type, scope: &mut Scope, contents: &str) { + if pattern.kind() == "identifier" { + let name = &contents[pattern.byte_range()]; + scope.insert(name.to_string(), type_); + } else if matches!(pattern.kind(), "pattern_list" | "tuple_pattern") { + if let Type::Tuple(mut inner) = type_ { + inner.reverse(); + for child in pattern.named_children(&mut pattern.walk()) { + if matches!(child.kind(), "identifier" | "tuple_pattern") + && let Some(type_) = inner.pop() + { + unpack_forloop_pattern(child, type_, scope, contents); + } + } + } else if let Some(inner) = Index::type_of_iterable(type_) { + // spread this type to all params + for child in pattern.named_children(&mut pattern.walk()) { + if matches!(child.kind(), "identifier" | "tuple_pattern") { + unpack_forloop_pattern(child, inner.clone(), scope, contents); + } + } + } + } +} + #[cfg(test)] mod tests { use pretty_assertions::assert_eq; diff --git a/src/backend.rs b/src/backend.rs index ab0bd72..921ffec 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -972,7 +972,7 @@ impl Index { fomat! { if let Some(name) = model_name { "```python\n" - if let Some(ident) = identifier { (ident) ": " } "Model[\"" (name) "\"]\n" + if let Some(ident) = identifier { "(variable) " (ident) ": " } "Model[\"" (name) "\"]\n" "``` \n" } if let Some(module) = module { diff --git a/src/index.rs b/src/index.rs index c352b2a..65c177c 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1099,7 +1099,7 @@ pub fn index_models(contents: &[u8]) -> anyhow::Result> { }); match &contents[capture.byte_range()] { b"_name" => { - let Some(name_decl) = capture.next_named_sibling() else { + let Some(name_decl) = python_next_named_sibling(capture) else { continue; }; if name_decl.kind() == "string" { @@ -1111,7 +1111,7 @@ pub fn index_models(contents: &[u8]) -> anyhow::Result> { } } b"_inherit" => { - let Some(inherit_decl) = capture.next_named_sibling() else { + let Some(inherit_decl) = python_next_named_sibling(capture) else { continue; }; match inherit_decl.kind() { @@ -1137,7 +1137,7 @@ pub fn index_models(contents: &[u8]) -> anyhow::Result> { } } b"_inherits" => { - let Some(inherit_dict) = capture.next_named_sibling() else { + let Some(inherit_dict) = python_next_named_sibling(capture) else { continue; }; if inherit_dict.kind() != "dictionary" { diff --git a/src/index/js.rs b/src/index/js.rs index a00d483..247b57d 100644 --- a/src/index/js.rs +++ b/src/index/js.rs @@ -1,3 +1,5 @@ +#![allow(clippy::disallowed_methods)] + use std::ops::DerefMut; use std::{collections::HashMap, path::PathBuf}; diff --git a/src/model.rs b/src/model.rs index b772787..c7028b7 100644 --- a/src/model.rs +++ b/src/model.rs @@ -432,9 +432,10 @@ impl ModelIndex { ) -> Option> { let model_name = _R(model); let Some(mut entry) = self.try_get_mut(&model).try_unwrap() else { + cold_path(); panic!("{} deadlock on model {}", loc!(), _R(model)); }; - if entry.fields.is_some() && entry.methods.is_some() && locations_filter.is_empty() { + if likely(entry.fields.is_some() && entry.methods.is_some() && locations_filter.is_empty()) { return Some(entry); } let t0 = std::time::Instant::now(); diff --git a/src/python.rs b/src/python.rs index 37796ac..5070f40 100644 --- a/src/python.rs +++ b/src/python.rs @@ -650,7 +650,7 @@ impl Backend { } let model = _R(model); return self.index.jump_def_property_name(needle, model); - } else if let Some(cmdlist) = capture.node.next_named_sibling() + } else if let Some(cmdlist) = python_next_named_sibling(capture.node) && Backend::is_commandlist(cmdlist, offset) { let (needle, _, model) = some!(self.gather_commandlist( @@ -667,7 +667,7 @@ impl Backend { } } Some(PyCompletions::FieldDescriptor) => { - let Some(desc_value) = capture.node.next_named_sibling() else { + let Some(desc_value) = python_next_named_sibling(capture.node) else { continue; }; @@ -780,7 +780,7 @@ impl Backend { let idx = contents[..=offset].bytes().rposition(|c| c == b'.')?; let ident = contents[..=idx].bytes().rposition(|c| c.is_ascii_alphanumeric())?; lhs = root.descendant_for_byte_range(ident, ident)?; - rhs = lhs.next_named_sibling().and_then(|attr| match attr.kind() { + rhs = python_next_named_sibling(lhs).and_then(|attr| match attr.kind() { "identifier" => Some(attr), "attribute" => attr.child_by_field_name("attribute"), _ => None, @@ -894,7 +894,7 @@ impl Backend { } } Some(PyCompletions::FieldDescriptor) => { - let Some(desc_value) = capture.node.next_named_sibling() else { + let Some(desc_value) = python_next_named_sibling(capture.node) else { continue; }; let descriptor = &contents[range]; @@ -1001,7 +1001,7 @@ impl Backend { } let model = _R(model); return (self.index).hover_property_name(needle, model, Some(rope_conv(range, rope))); - } else if let Some(cmdlist) = capture.node.next_named_sibling() + } else if let Some(cmdlist) = python_next_named_sibling(capture.node) && Backend::is_commandlist(cmdlist, offset) { let (needle, range, model) = some!(self.gather_commandlist( @@ -1042,7 +1042,7 @@ impl Backend { return self.index.hover_property_name(name, model, Some(range)); } Some(PyCompletions::FieldDescriptor) => { - let Some(desc_value) = capture.node.next_named_sibling() else { + let Some(desc_value) = python_next_named_sibling(capture.node) else { continue; }; let descriptor = &contents[range]; diff --git a/src/python/completions.rs b/src/python/completions.rs index 0d0a82b..35f6193 100644 --- a/src/python/completions.rs +++ b/src/python/completions.rs @@ -269,7 +269,7 @@ impl Backend { Some(PropertyKind::Field), rope, ); - } else if let Some(cmdlist) = capture.node.next_named_sibling() + } else if let Some(cmdlist) = python_next_named_sibling(capture.node) && Backend::is_commandlist(cmdlist, offset) && let Some((needle, range, model)) = self.gather_commandlist( cmdlist, @@ -300,7 +300,7 @@ impl Backend { } } Some(PyCompletions::FieldDescriptor) => { - let Some(desc_value) = capture.node.next_named_sibling() else { + let Some(desc_value) = python_next_named_sibling(capture.node) else { continue; }; diff --git a/src/python/diagnostics.rs b/src/python/diagnostics.rs index 5c6419f..5d2fad4 100644 --- a/src/python/diagnostics.rs +++ b/src/python/diagnostics.rs @@ -176,7 +176,7 @@ impl Backend { Some(PyCompletions::FieldDescriptor) => { // fields.Many2one(field_descriptor=...) - let Some(desc_value) = capture.node.next_named_sibling() else { + let Some(desc_value) = python_next_named_sibling(capture.node) else { continue; }; diff --git a/src/utils.rs b/src/utils.rs index 193593c..9e62f30 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -44,24 +44,23 @@ macro_rules! some { macro_rules! dig { () => { None }; ($start:expr, $($rest:tt)+) => { - dig!(@inner Some($start), $($rest)+) + $crate::dig!(@inner Some($start), $($rest)+) }; (@inner $node:expr, $kind:ident($idx:literal).$($rest:tt)+) => { - dig!( + $crate::dig!( @inner - if let Some(node) = $node && let Some(child) = node.named_child($idx) && child.kind() == stringify!($kind) { Some(child) } else { None }, + if let Some(node) = $node && let Some(child) = $crate::utils::python_nth_named_child_matching::<$idx>(node, stringify!($kind)) { Some(child) } else { None }, $($rest)+ ) }; - (@inner $node:expr, $kind:ident.$($rest:tt)+) => { - dig!(@inner $node, $kind(0).$($rest)+) + $crate::dig!(@inner $node, $kind(0).$($rest)+) }; (@inner $node:expr, $kind:ident($idx:literal)) => { - if let Some(node) = $node && let Some(child) = node.named_child($idx) && child.kind() == stringify!($kind) { Some(child) } else { None } + if let Some(node) = $node && let Some(child) = $crate::utils::python_nth_named_child_matching::<$idx>(node, stringify!($kind)) { Some(child) } else { None } }; (@inner $node:expr, $kind:ident) => { - dig!(@inner $node, $kind(0)) + $crate::dig!(@inner $node, $kind(0)) }; } @@ -296,6 +295,62 @@ pub fn cow_split_once<'src>( } } +#[inline(always)] +#[cold] +pub const fn cold_path() {} + +/// Copied from https://github.com/rust-lang/hashbrown/commit/64bd7db1d1b148594edfde112cdb6d6260e2cfc3 +#[inline(always)] +pub const fn likely(cond: bool) -> bool { + if cond { + true + } else { + cold_path(); + false + } +} + +#[inline(always)] +pub const fn unlikely(cond: bool) -> bool { + if !cond { + true + } else { + cold_path(); + false + } +} + +/// Only useful for Python, since the default grammar does not mark comments as extra nodes. +#[allow(clippy::disallowed_methods)] +#[inline] +pub fn python_next_named_sibling(mut node: Node) -> Option { + loop { + node = node.next_named_sibling()?; + if likely(node.kind() != "comment") { + return Some(node); + } + } +} + +#[allow(clippy::disallowed_methods)] +#[inline] +pub fn python_nth_named_child_matching<'node, const NTH: usize>( + mut node: Node<'node>, + kind: &'static str, +) -> Option> { + let mut idx = 0; + node = node.named_child(0)?; + loop { + if idx == NTH && likely(node.kind() == kind) { + return Some(node); + } + if likely(node.kind() != "comment") { + idx += 1; + } + node = node.next_named_sibling()?; + } +} + #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] #[repr(transparent)] pub struct ByteOffset(pub usize); @@ -695,4 +750,28 @@ mod tests { let rhs = strict_canonicalize(&lhs).unwrap(); assert_eq!(lhs, rhs); } + + #[test] + fn test_python_first_nth_child_matching() { + use tree_sitter::Parser; + use tree_sitter_python::LANGUAGE; + + let contents = r#"[ + # A comment + 1, + # Another comment + 2, + 3, + } + }"#; + + let mut parser = Parser::new(); + parser.set_language(&LANGUAGE.into()).unwrap(); + let tree = parser.parse(contents, None).unwrap(); + let root = tree.root_node(); + let list_node = root.named_child(0).unwrap(); + let first_element = list_node.named_child(1).unwrap(); + let second_element = super::python_nth_named_child_matching::<0>(list_node, "integer").unwrap(); + pretty_assertions::assert_eq!(first_element, second_element); + } } diff --git a/src/utils/lru.rs b/src/utils/lru.rs deleted file mode 100644 index 845e91a..0000000 --- a/src/utils/lru.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::collections::{HashMap, VecDeque}; - -pub struct LruCache { - map: HashMap, - order: VecDeque, - capacity: usize, -} - -impl Default for LruCache -where - K: Clone + Eq + std::hash::Hash, -{ - #[inline] - fn default() -> Self { - Self::new(16) - } -} - -impl LruCache { - pub fn new(capacity: usize) -> Self { - Self { - map: Default::default(), - order: Default::default(), - capacity, - } - } - - pub fn get(&mut self, key: &K) -> Option<&V> { - let item = self.map.get(key); - if item.is_some() { - // let mut order = self.order.write(); - // move key to the back (most recent) - self.order.retain(|k| k != key); - self.order.push_back(key.clone()); - } - item - } - - pub fn put(&mut self, key: K, value: V) { - if self.map.contains_key(&key) { - // update existing, move key - self.order.retain(|k| k != &key); - } else if self.map.len() == self.capacity { - // evict least recent - if let Some(old) = self.order.pop_front() { - self.map.remove(&old); - } - } - self.map.insert(key.clone(), value); - self.order.push_back(key); - } -} diff --git a/testing/fixtures/python_types/bakery/models/models.py b/testing/fixtures/python_types/bakery/models/models.py index 908af4e..99f6776 100644 --- a/testing/fixtures/python_types/bakery/models/models.py +++ b/testing/fixtures/python_types/bakery/models/models.py @@ -68,3 +68,43 @@ def test_sanity(self): #^type List(PyBuiltin("str")) self._fields #^type Dict([PyBuiltin("str"), Model("ir.model.fields")]) + + def test_builtins(self): + for aaaa, bbbb in enumerate(self): + aaaa + #^type PyBuiltin("int") + bbbb + #^type Model("bakery.bread") + + ints = [1, 2, 3] + for aaaa, bbbb in zip(self, ints): + aaaa + #^type Model("bakery.bread") + bbbb + #^type PyBuiltin("int") + + what = [ + 123 for + cccc, + #^type Model("bakery.bread") + dddd + #^type PyBuiltin("int") + in zip(self, ints) + ] + + +class Wine(models.Model): + _name = 'bakery.wine' + + name = fields.Char() + make = fields.Char() + value = fields.Float() + + def _test(self): + domain = [] + for name, make, value in self._read_group(domain, ['name', 'make'], ['value:sum']): + #^type PyBuiltin("str") + make + #^type PyBuiltin("str") + value + #^type PyBuiltin("float") From 869106caab7b042533f6b6a575f3c445d4447694 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Tue, 11 Nov 2025 22:37:40 -0500 Subject: [PATCH 03/15] type display --- src/analyze.rs | 26 +++++++++++++++++--------- src/model.rs | 17 ++++++++++++----- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/analyze.rs b/src/analyze.rs index 2227d35..38517e4 100644 --- a/src/analyze.rs +++ b/src/analyze.rs @@ -980,31 +980,37 @@ impl Index { _ => None, } } + #[inline] pub fn type_display<'a>(&self, type_: &'a Type) -> Option> { + self.type_display_indent(type_, 0) + } + fn type_display_indent<'a>(&self, type_: &'a Type, indent: usize) -> Option> { match type_ { Type::Dict(pair) => { let [lhs, rhs] = &**pair; - let lhs = self.type_display(lhs); + let lhs = self.type_display_indent(lhs, indent); let lhs = lhs.as_deref().unwrap_or("..."); - let rhs = self.type_display(rhs); + let rhs = self.type_display_indent(rhs, indent); let rhs = rhs.as_deref().unwrap_or("..."); Some(fomat! { "dict[" (lhs) ", " (rhs) "]" }.into()) } Type::DictBag(properties) => { + let preindent = " ".repeat(indent + 2); let properties_fragment = fomat! { for (key, value) in properties { - " " + (preindent) match key { DictKey::String(key) => { "\"" (key) "\"" } DictKey::Type(Type::Dict(..) | Type::DictBag(..)) => { "{...}" } - DictKey::Type(key) => { (self.type_display(key).as_deref().unwrap_or("...")) } - } ": " (self.type_display(value).as_deref().unwrap_or("...")) + DictKey::Type(key) => { (self.type_display_indent(key, indent + 2).as_deref().unwrap_or("...")) } + } ": " (self.type_display_indent(value, indent + 2).as_deref().unwrap_or("...")) } sep { ",\n" } }; + let unindent = " ".repeat(indent); Some( fomat! { if !properties.is_empty() { - "{\n" (properties_fragment) "\n}" + "{\n" (properties_fragment) "\n" (unindent) "}" } else { "{}" } @@ -1016,7 +1022,7 @@ impl Index { Type::List(slot) => { let slot = match slot { ListElement::Vacant => None, - ListElement::Occupied(slot) => self.type_display(slot), + ListElement::Occupied(slot) => self.type_display_indent(slot, indent), }; Some(match slot { Some(slot) => format!("list[{slot}]").into(), @@ -1034,14 +1040,16 @@ impl Index { fomat! { "tuple[" for item in items { - (self.type_display(item).as_deref().unwrap_or("...")) + (self.type_display_indent(item, indent).as_deref().unwrap_or("...")) } sep { ", " } "]" } .into(), ), Type::Iterable(output) => { - let output = output.as_deref().and_then(|inner| self.type_display(inner)); + let output = output + .as_deref() + .and_then(|inner| self.type_display_indent(inner, indent)); let output = output.as_deref().unwrap_or("..."); Some(format!("Iterable[{output}]").into()) } diff --git a/src/model.rs b/src/model.rs index c7028b7..8a9403f 100644 --- a/src/model.rs +++ b/src/model.rs @@ -11,6 +11,7 @@ use std::sync::atomic::AtomicBool; use dashmap::DashMap; use dashmap::mapref::one::RefMut; +use dashmap::try_result::TryResult; use derive_more::{Deref, DerefMut}; use qp_trie::Trie; use rayon::prelude::{IntoParallelIterator, ParallelIterator}; @@ -430,10 +431,15 @@ impl ModelIndex { model: ModelName, locations_filter: &[PathSymbol], ) -> Option> { - let model_name = _R(model); - let Some(mut entry) = self.try_get_mut(&model).try_unwrap() else { - cold_path(); - panic!("{} deadlock on model {}", loc!(), _R(model)); + let mut entry = match self.try_get_mut(&model) { + TryResult::Present(entry) => entry, + TryResult::Absent => { + return None; + } + TryResult::Locked => { + cold_path(); + panic!("{} deadlock on model {}", loc!(), _R(model)); + } }; if likely(entry.fields.is_some() && entry.methods.is_some() && locations_filter.is_empty()) { return Some(entry); @@ -707,11 +713,12 @@ impl ModelIndex { }); } + let model_name = _R(model); info!( "{model_name}: {} fields, {} methods, {}ms", out_fields.len(), out_methods.len(), - t0.elapsed().as_millis() + t0.elapsed().as_millis(), ); let mut entry = self.try_get_mut(&model).expect(format_loc!("deadlock")).unwrap(); entry.fields = Some(out_fields); From 2fe58286fe419f80d54117bfb14edae14027fd93 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Fri, 14 Nov 2025 10:40:23 -0500 Subject: [PATCH 04/15] expression_list and destructure into pattern_list --- src/analyze.rs | 27 ++++++++++++++----- .../python_types/bakery/models/models.py | 9 +++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/analyze.rs b/src/analyze.rs index 38517e4..5dd7f73 100644 --- a/src/analyze.rs +++ b/src/analyze.rs @@ -299,6 +299,11 @@ impl Index { } else { properties.push((DictKey::String(ImStr::from(key)), type_)); } + } else if lhs.kind() == "pattern_list" + && let Some(rhs) = python_next_named_sibling(lhs) + && let Some(type_) = self.type_of(rhs, scope, contents) + { + destructure_into_patternlist_like(lhs, type_, scope, contents); } } "for_statement" => { @@ -310,7 +315,7 @@ impl Index { && let Some(type_) = self.type_of(rhs, scope, contents) && let Some(inner) = Self::type_of_iterable(type_) { - unpack_forloop_pattern(lhs, inner, scope, contents); + destructure_into_patternlist_like(lhs, inner, scope, contents); } return ControlFlow::Continue(true); } @@ -341,7 +346,7 @@ impl Index { && let Some(type_) = self.type_of(rhs, scope, contents) && let Some(inner) = Self::type_of_iterable(type_) { - unpack_forloop_pattern(lhs, inner, scope, contents); + destructure_into_patternlist_like(lhs, inner, scope, contents); } } "call" if node.byte_range().contains_end(offset) => { @@ -611,7 +616,7 @@ impl Index { { // FIXME: How to prevent this clone? comprehension_scope = Scope::new(Some(scope.clone())); - unpack_forloop_pattern(scrutinee, iter_ty, &mut comprehension_scope, contents); + destructure_into_patternlist_like(scrutinee, iter_ty, &mut comprehension_scope, contents); pair_scope = &comprehension_scope; } let lhs = pair @@ -663,6 +668,16 @@ impl Index { } Some(Type::List(slot)) } + "expression_list" => { + let mut cursor = node.walk(); + let tuple = node.named_children(&mut cursor).filter_map(|child| { + if child.kind() == "comment" { + return None; + } + Some(self.type_of(child, scope, contents).unwrap_or(Type::Value)) + }); + Some(Type::Tuple(tuple.collect())) + } "string" => Some(Type::PyBuiltin(ImStr::from_static("str"))), "integer" => Some(Type::PyBuiltin(ImStr::from_static("int"))), "float" => Some(Type::PyBuiltin(ImStr::from_static("float"))), @@ -1289,7 +1304,7 @@ pub fn determine_scope<'out, 'node>( } /// `pattern` is `(identifier | pattern_list | tuple_pattern)`, the `a, b` in `for a, b in ...`. -fn unpack_forloop_pattern(pattern: Node, type_: Type, scope: &mut Scope, contents: &str) { +fn destructure_into_patternlist_like(pattern: Node, type_: Type, scope: &mut Scope, contents: &str) { if pattern.kind() == "identifier" { let name = &contents[pattern.byte_range()]; scope.insert(name.to_string(), type_); @@ -1300,14 +1315,14 @@ fn unpack_forloop_pattern(pattern: Node, type_: Type, scope: &mut Scope, content if matches!(child.kind(), "identifier" | "tuple_pattern") && let Some(type_) = inner.pop() { - unpack_forloop_pattern(child, type_, scope, contents); + destructure_into_patternlist_like(child, type_, scope, contents); } } } else if let Some(inner) = Index::type_of_iterable(type_) { // spread this type to all params for child in pattern.named_children(&mut pattern.walk()) { if matches!(child.kind(), "identifier" | "tuple_pattern") { - unpack_forloop_pattern(child, inner.clone(), scope, contents); + destructure_into_patternlist_like(child, inner.clone(), scope, contents); } } } diff --git a/testing/fixtures/python_types/bakery/models/models.py b/testing/fixtures/python_types/bakery/models/models.py index 99f6776..09ca5bd 100644 --- a/testing/fixtures/python_types/bakery/models/models.py +++ b/testing/fixtures/python_types/bakery/models/models.py @@ -92,6 +92,15 @@ def test_builtins(self): in zip(self, ints) ] + def _identity_tuple(self, obj): + return self, obj + + def _test_tuple(self): + foo, bar = self._identity_tuple(123) + #^type Model("bakery.bread") + bar + #^type PyBuiltin("int") + class Wine(models.Model): _name = 'bakery.wine' From 209cf223efec2c33825c54431b3d7493a6c47c27 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Mon, 1 Dec 2025 10:09:10 -0500 Subject: [PATCH 05/15] dict key completion --- src/python/completions.rs | 38 +++++++++++++++++-- .../python_types/bakery/models/models.py | 7 ++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/python/completions.rs b/src/python/completions.rs index 35f6193..f88ca6b 100644 --- a/src/python/completions.rs +++ b/src/python/completions.rs @@ -10,6 +10,7 @@ use tree_sitter::Tree; use crate::prelude::*; +use crate::analyze::{DictKey, Type}; use crate::backend::Backend; use crate::index::{_G, _I, _R, symbol::Symbol}; use crate::model::{FieldKind, ModelEntry, ModelName, PropertyKind}; @@ -432,14 +433,12 @@ impl Backend { } } if early_return.is_none() { - // Check if we're in a broken syntax situation (string without colon in dictionary) let cursor_node = root.descendant_for_byte_range(offset, offset); if let Some(node) = cursor_node { - // Check if we're in a string that's part of an ERROR node let mut current = node; while let Some(parent) = current.parent() { + // (dictionary (ERROR ^cursor)) if parent.kind() == "ERROR" { - // Check if the ERROR's parent is a dictionary if let Some(grandparent) = parent.parent() && grandparent.kind() == "dictionary" { @@ -511,6 +510,39 @@ impl Backend { }))); } } + } else if current.kind() == "string" + && parent.kind() == "subscript" + && let Some(lhs) = parent.named_child(0) + && let Some((Type::DictBag(dict), _)) = + self.index + .type_of_range(root, dbg!(lhs).byte_range().map_unit(ByteOffset), &contents) + { + let mut items = MaxVec::new(completions_limit); + let dict = dict.into_iter().flat_map(|(key, _)| match key { + DictKey::String(str) => Some(str.to_string()), + _ => None, + }); + let range = current.byte_range().shrink(1).map_unit(ByteOffset); + let range = rope_conv(range, rope); + let to_item = |label: String| { + let new_text = label.clone(); + CompletionItem { + label, + kind: Some(CompletionItemKind::CONSTANT), + text_edit: Some(CompletionTextEdit::Edit(TextEdit { range, new_text })), + ..Default::default() + } + }; + if offset <= current.start_byte() + 1 { + items.extend(dict.map(to_item)); + } else { + let needle = &contents[current.start_byte() + 1..offset]; + items.extend(dict.filter(|label| label.starts_with(needle)).map(to_item)); + } + return Ok(Some(CompletionResponse::List(CompletionList { + is_incomplete: !items.has_space(), + items: items.into_inner(), + }))); } current = parent; } diff --git a/testing/fixtures/python_types/bakery/models/models.py b/testing/fixtures/python_types/bakery/models/models.py index 09ca5bd..90c39ff 100644 --- a/testing/fixtures/python_types/bakery/models/models.py +++ b/testing/fixtures/python_types/bakery/models/models.py @@ -101,6 +101,13 @@ def _test_tuple(self): bar #^type PyBuiltin("int") + def test_subscript(self): + foobar = {'abcde': 123, 'fool': 234} + foobar[''] + # ^complete abcde fool + foobar['f'] + # ^complete fool + class Wine(models.Model): _name = 'bakery.wine' From 9785e8c93801c64866729b3871f15454d3cfa2da Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Mon, 1 Dec 2025 10:09:18 -0500 Subject: [PATCH 06/15] grouped, dict.items --- src/analyze.rs | 81 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 10 deletions(-) diff --git a/src/analyze.rs b/src/analyze.rs index 5dd7f73..6fcf835 100644 --- a/src/analyze.rs +++ b/src/analyze.rs @@ -63,6 +63,8 @@ pub enum Type { Record(ImStr), Super, Method(ModelName, ImStr), + /// To hardcode some methods, such as dict.items() + PythonMethod(Box, ImStr), /// `odoo.http.request` HttpRequest, Dict(Box<[Type; 2]>), @@ -77,6 +79,17 @@ pub enum Type { Value, } +impl Type { + #[inline] + fn list_of(inner: Type) -> Self { + Type::List(ListElement::Occupied(Box::new(inner))) + } + #[inline] + fn is_dict(&self) -> bool { + matches!(self, Type::Dict(_)) + } +} + #[derive(Clone, PartialEq, Eq)] pub enum ListElement { Vacant, @@ -186,7 +199,7 @@ query! { (identifier) @_func (lambda (lambda_parameters . (identifier) @ITER)))])) (#match? @_func "^(func|key)$") - (#match? @_mapped "^(mapp|filter|sort)ed$")) + (#match? @_mapped "^(mapp|filter|sort|group)ed$")) } #[rustfmt::skip] @@ -668,7 +681,7 @@ impl Index { } Some(Type::List(slot)) } - "expression_list" => { + "expression_list" | "tuple" => { let mut cursor = node.walk(); let tuple = node.named_children(&mut cursor).filter_map(|child| { if child.kind() == "comment" { @@ -694,6 +707,12 @@ impl Index { _ => None, } } + fn wrap_in_container Type>(type_: Type, producer: F) -> Type { + match type_ { + Type::Model(..) => type_, + _ => producer(type_), + } + } fn type_of_call_node(&self, call: Node<'_>, scope: &Scope, contents: &str) -> Option { let func = call.named_child(0)?; if func.kind() == "identifier" { @@ -753,7 +772,7 @@ impl Index { let mut model: Spur = model.into(); let mut mapped = &contents[mapped.byte_range().shrink(1)]; self.models.resolve_mapped(&mut model, &mut mapped, None).ok()?; - self.type_of_attribute(&Type::Model(_R(model).into()), mapped, scope) + self.type_of_attribute(&Type::Model(ImStr::from_static(_R(model))), mapped, scope) } "lambda" => { // (lambda (lambda_parameters)? body: (_)) @@ -762,11 +781,41 @@ impl Index { let first_arg = params.named_child(0)?; if first_arg.kind() == "identifier" { let first_arg = &contents[first_arg.byte_range()]; - scope.insert(first_arg.to_string(), Type::Model(_R(model).into())); + scope.insert(first_arg.to_string(), Type::Model(ImStr::from_static(_R(model)))); } } let body = mapped.child_by_field_name(b"body")?; - self.type_of(body, &scope, contents) + let type_ = self.type_of(body, &scope, contents).unwrap_or(Type::Value); + Some(Index::wrap_in_container(type_, Type::list_of)) + } + _ => None, + } + } + Type::Method(model, grouped) if grouped == "grouped" => { + // (call (_) @func (argument_list . [(string) (lambda)] @mapped)) + let grouped = call.named_child(1)?.named_child(0)?; + match grouped.kind() { + "string" => { + let mut model: Spur = model.into(); + let mut grouped = &contents[grouped.byte_range().shrink(1)]; + self.models.resolve_mapped(&mut model, &mut grouped, None).ok()?; + let model = Type::Model(ImStr::from_static(_R(model))); + let groupby = self.type_of_attribute(&model, grouped, scope)?; + Some(Type::Dict(Box::new([groupby, model]))) + } + "lambda" => { + let mut scope = Scope::new(Some(scope.clone())); + if let Some(params) = grouped.child_by_field_name(b"parameters") { + let first_arg = params.named_child(0)?; + if first_arg.kind() == "identifier" { + let first_arg = &contents[first_arg.byte_range()]; + scope.insert(first_arg.to_string(), Type::Model(ImStr::from_static(_R(model)))); + } + } + let body = grouped.child_by_field_name(b"body")?; + let groupby = self.type_of(body, &scope, contents).unwrap_or(Type::Value); + let model = Type::Model(ImStr::from_static(_R(model))); + Some(Type::Dict(Box::new([groupby, model]))) } _ => None, } @@ -828,6 +877,10 @@ impl Index { let args = self.prepare_call_scope(model, method.into(), call, scope, contents); self.eval_method_rtype(method.into(), *model, args) } + Type::PythonMethod(dict, items) if dict.is_dict() && items == "items" => { + let Type::Dict(dict) = *dict else { unreachable!() }; + Some(Type::Iterable(Some(Box::new(Type::Tuple(dict.to_vec()))))) + } Type::Env | Type::Record(..) | Type::Model(..) @@ -838,7 +891,8 @@ impl Index { | Type::DictBag(..) | Type::List(..) | Type::Iterable(..) - | Type::Tuple(..) => None, + | Type::Tuple(..) + | Type::PythonMethod(..) => None, } } @@ -902,12 +956,13 @@ impl Index { match &contents[rhs.byte_range()] { "env" if matches!(lhs, Type::Model(..) | Type::Record(..) | Type::HttpRequest) => Some(Type::Env), "ref" if matches!(lhs, Type::Env) => Some(Type::RefFn), - "user" if matches!(lhs, Type::Env) => Some(Type::Model("res.users".into())), - "company" | "companies" if matches!(lhs, Type::Env) => Some(Type::Model("res.company".into())), - "mapped" | "_read_group" => { + "user" if matches!(lhs, Type::Env) => Some(Type::Model(ImStr::from_static("res.users"))), + "company" | "companies" if matches!(lhs, Type::Env) => Some(Type::Model(ImStr::from_static("res.company"))), + "mapped" | "grouped" | "_read_group" => { let model = self.try_resolve_model(&lhs, scope)?; Some(Type::Method(model, attrname.into())) } + dict_method @ "items" if lhs.is_dict() => Some(Type::PythonMethod(Box::new(lhs), dict_method.into())), func if MODEL_METHODS.contains(func) => match lhs { Type::Model(model) => Some(Type::ModelFn(model)), Type::Record(xml_id) => { @@ -1069,7 +1124,13 @@ impl Index { Some(format!("Iterable[{output}]").into()) } Type::Method(..) => unreachable!("Bug: this function should not handle methods"), - Type::RefFn | Type::ModelFn(_) | Type::Super | Type::HttpRequest | Type::Value => None, + Type::RefFn | Type::ModelFn(_) | Type::Super | Type::HttpRequest | Type::Value | Type::PythonMethod(..) => { + if cfg!(debug_assertions) { + Some(format!("{type_:?}").into()) + } else { + None + } + } } } /// Iterates depth-first over `node` using [`PreTravel`]. Automatically calls [`Scope::exit`] at suitable points. From 21c763acbe0e9bafae90bb29cb7a143130bd32bd Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Mon, 1 Dec 2025 23:09:16 -0500 Subject: [PATCH 07/15] add unit testing for grouped --- src/index/symbol.rs | 6 ++++-- src/python/diagnostics.rs | 17 ++++------------- .../python_types/bakery/models/models.py | 10 ++++++++++ 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/index/symbol.rs b/src/index/symbol.rs index 25eb4fe..5eafc43 100644 --- a/src/index/symbol.rs +++ b/src/index/symbol.rs @@ -22,9 +22,11 @@ pub struct Symbol { pub struct PathSymbol(Spur, Spur); impl PathSymbol { - /// Panics if `root` is not a parent of `path`. + /// Creates a path symbol from a path and its prefix. + /// + /// Panics if `root` is not a parent/prefix of `path`. pub fn strip_root(root: Spur, path: &Path) -> Self { - let path = path.strip_prefix(interner().resolve(&root)).unwrap(); + let path = path.strip_prefix(_R(root)).unwrap(); let path = _I(path.to_string_lossy()); PathSymbol(root, path) } diff --git a/src/python/diagnostics.rs b/src/python/diagnostics.rs index 5d2fad4..72218c7 100644 --- a/src/python/diagnostics.rs +++ b/src/python/diagnostics.rs @@ -321,20 +321,11 @@ impl Backend { } let attribute = attribute.unwrap(); + #[rustfmt::skip] static MODEL_BUILTINS: phf::Set<&str> = phf::phf_set!( - "env", - "id", - "ids", - "display_name", - "create_date", - "write_date", - "create_uid", - "write_uid", - "pool", - "record", - "flush_model", - "mapped", - "fields_get", + "env", "id", "ids", "display_name", "create_date", "write_date", + "create_uid", "write_uid", "pool", "record", "flush_model", "mapped", + "grouped", "_read_group", "filtered", "sorted", "_origin", "fields_get", "user_has_groups", ); let prop = &contents[attribute.byte_range()]; diff --git a/testing/fixtures/python_types/bakery/models/models.py b/testing/fixtures/python_types/bakery/models/models.py index 90c39ff..d9144cd 100644 --- a/testing/fixtures/python_types/bakery/models/models.py +++ b/testing/fixtures/python_types/bakery/models/models.py @@ -124,3 +124,13 @@ def _test(self): #^type PyBuiltin("str") value #^type PyBuiltin("float") + + def test_grouped(self): + for name, records in self.grouped('name').items(): + #^type PyBuiltin("str") + records + #^type Model("bakery.wine") + for (some, thing), _records in self.grouped(lambda mov: (mov.name, mov.value)).items(): + #^type PyBuiltin("str") + thing + #^type PyBuiltin("float") From 3685711c1b366ceb43d56301e24c03b16fae7599 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Tue, 2 Dec 2025 21:55:35 -0500 Subject: [PATCH 08/15] typeid --- Cargo.lock | 857 +++++++++--------- Cargo.toml | 6 +- src/analyze.rs | 504 +++++----- src/backend.rs | 15 +- src/model.rs | 13 +- src/python.rs | 11 +- src/python/completions.rs | 9 +- src/python/diagnostics.rs | 8 +- src/server.rs | 30 + src/str.rs | 10 +- src/xml.rs | 18 +- testing/Cargo.toml | 2 +- .../python_types/bakery/models/models.py | 4 +- testing/src/tests.rs | 4 +- 14 files changed, 788 insertions(+), 703 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aaa15c6..8a12a1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] @@ -31,9 +31,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -46,18 +46,18 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" dependencies = [ "backtrace", ] [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] @@ -100,17 +100,11 @@ dependencies = [ "topological-sort", ] -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - [[package]] name = "backtrace" -version = "0.3.75" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", @@ -118,7 +112,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -144,11 +138,11 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.72.0" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "cexpr", "clang-sys", "itertools", @@ -170,9 +164,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "block-buffer" @@ -185,9 +179,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "serde", @@ -207,15 +201,15 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "camino" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" dependencies = [ "serde_core", ] @@ -244,10 +238,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.30" +version = "1.2.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" dependencies = [ + "find-msvc-tools", "shlex", ] @@ -262,9 +257,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -304,9 +299,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const_format" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" dependencies = [ "const_format_proc_macros", ] @@ -376,9 +371,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -456,18 +451,18 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] [[package]] name = "derive_arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", @@ -476,21 +471,22 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" dependencies = [ "proc-macro2", "quote", + "rustc_version", "syn", ] @@ -566,12 +562,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -597,21 +593,27 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" -version = "0.2.25" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "miniz_oxide", @@ -646,9 +648,9 @@ checksum = "3f722aa875298d34a0ebb6004699f6f4ea830d36dec8ac2effdbbc840248a096" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -767,29 +769,29 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "git-version" @@ -813,21 +815,21 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] @@ -836,16 +838,16 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "ignore", "walkdir", ] [[package]] name = "h2" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -872,13 +874,19 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heck" version = "0.5.0" @@ -887,12 +895,11 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -927,19 +934,21 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -964,9 +973,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ "base64", "bytes", @@ -980,7 +989,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2", "tokio", "tower-service", "tracing", @@ -1031,9 +1040,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -1044,9 +1053,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -1057,11 +1066,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -1072,42 +1080,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -1123,9 +1127,9 @@ checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1144,15 +1148,15 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.23" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" dependencies = [ "crossbeam-deque", "globset", "log", "memchr", - "regex-automata 0.4.9", + "regex-automata", "same-file", "walkdir", "winapi-util", @@ -1160,13 +1164,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.16.1", "serde", + "serde_core", ] [[package]] @@ -1184,20 +1189,9 @@ dependencies = [ [[package]] name = "intmap" -version = "3.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16dd999647b7a027fadf2b3041a4ea9c8ae21562823fe5cbdecd46537d535ae2" - -[[package]] -name = "io-uring" -version = "0.7.9" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" -dependencies = [ - "bitflags 2.9.1", - "cfg-if", - "libc", -] +checksum = "a2e611826a1868311677fdcdfbec9e8621d104c732d080f546a854530232f0ee" [[package]] name = "ipnet" @@ -1207,9 +1201,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -1232,9 +1226,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -1264,58 +1258,57 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libloading" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.53.2", + "windows-link", ] [[package]] name = "libredox" -version = "0.1.6" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "libc", "redox_syscall", ] [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru-slab" @@ -1351,18 +1344,18 @@ dependencies = [ [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mini-moka" @@ -1392,17 +1385,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -1423,12 +1417,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -1439,9 +1432,9 @@ checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ "num_enum_derive", "rustversion", @@ -1449,9 +1442,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1467,9 +1460,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] @@ -1479,7 +1472,7 @@ name = "odoo-lsp" version = "0.6.1" dependencies = [ "anyhow", - "bitflags 2.9.1", + "bitflags 2.10.0", "const_format", "dashmap 6.1.0", "derive_more", @@ -1553,23 +1546,17 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -1580,9 +1567,9 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "phf" @@ -1657,9 +1644,9 @@ checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -1691,9 +1678,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.36" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", @@ -1701,9 +1688,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ "toml_edit", ] @@ -1732,9 +1719,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -1758,7 +1745,7 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "memchr", "unicase", ] @@ -1784,9 +1771,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", @@ -1795,7 +1782,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2", "thiserror", "tokio", "tracing", @@ -1804,12 +1791,12 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", - "getrandom 0.3.3", + "getrandom 0.3.4", "lru-slab", "rand", "ring", @@ -1825,23 +1812,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -1887,14 +1874,14 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -1902,9 +1889,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -1912,56 +1899,41 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.15" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "relative-path" @@ -1971,9 +1943,9 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "reqwest" -version = "0.12.22" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64", "bytes", @@ -2065,9 +2037,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -2086,22 +2058,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.29" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "once_cell", "ring", @@ -2113,9 +2085,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ "web-time", "zeroize", @@ -2123,9 +2095,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", @@ -2134,9 +2106,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -2196,18 +2168,19 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", + "serde_core", ] [[package]] name = "serde" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -2215,18 +2188,18 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -2235,15 +2208,16 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.141" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "indexmap", "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -2334,9 +2308,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -2357,22 +2331,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2396,9 +2360,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "str_indices" @@ -2441,9 +2405,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.104" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -2489,31 +2453,31 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.20.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -2531,9 +2495,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "num-conv", @@ -2544,15 +2508,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -2560,9 +2524,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -2575,27 +2539,24 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.46.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "pin-project-lite", - "slab", - "socket2 0.5.10", + "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -2604,9 +2565,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -2614,9 +2575,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -2628,18 +2589,31 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] [[package]] name = "toml_edit" -version = "0.22.27" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap", "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ "winnow", ] @@ -2666,11 +2640,11 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -2690,9 +2664,9 @@ checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-lsp-server" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cd168c085174eafa7492a519715f2d59436dc28cdfd9d13a5b864246899db9" +checksum = "88f3f8ec0dcfdda4d908bad2882fe0f89cf2b606e78d16491323e918dfa95765" dependencies = [ "bytes", "dashmap 6.1.0", @@ -2717,9 +2691,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -2728,9 +2702,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -2739,9 +2713,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -2770,14 +2744,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "serde", "serde_json", "sharded-slab", @@ -2791,13 +2765,13 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.25.8" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7b8994f367f16e6fa14b5aebbcb350de5d7cbea82dc5b00ae997dd71680dd2" +checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87" dependencies = [ "cc", "regex", - "regex-syntax 0.8.5", + "regex-syntax", "serde_json", "streaming-iterator", "tree-sitter-language", @@ -2850,9 +2824,9 @@ dependencies = [ [[package]] name = "triomphe" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" [[package]] name = "try-lock" @@ -2882,9 +2856,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicase" @@ -2894,15 +2868,15 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" @@ -2927,9 +2901,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -2957,9 +2931,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "version-compare" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" [[package]] name = "version_check" @@ -3009,45 +2983,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt 0.39.0", + "wit-bindgen 0.46.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", @@ -3058,9 +3019,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3068,22 +3029,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -3123,17 +3084,17 @@ version = "0.227.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" dependencies = [ - "bitflags 2.9.1", - "hashbrown 0.15.4", + "bitflags 2.10.0", + "hashbrown 0.15.5", "indexmap", "semver", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", @@ -3151,43 +3112,27 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" @@ -3213,7 +3158,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.2", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", ] [[package]] @@ -3234,18 +3188,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -3256,9 +3211,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -3268,9 +3223,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -3280,9 +3235,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -3292,9 +3247,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -3304,9 +3259,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -3316,9 +3271,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -3328,9 +3283,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -3340,15 +3295,15 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -3359,10 +3314,16 @@ version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" dependencies = [ - "wit-bindgen-rt 0.41.0", + "wit-bindgen-rt", "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "wit-bindgen-core" version = "0.41.0" @@ -3374,22 +3335,13 @@ dependencies = [ "wit-parser", ] -[[package]] -name = "wit-bindgen-rt" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.1", -] - [[package]] name = "wit-bindgen-rt" version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "futures", "once_cell", ] @@ -3432,7 +3384,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" dependencies = [ "anyhow", - "bitflags 2.9.1", + "bitflags 2.10.0", "indexmap", "log", "serde", @@ -3464,15 +3416,15 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "xattr" -version = "1.5.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", "rustix", @@ -3492,11 +3444,10 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -3504,9 +3455,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -3522,23 +3473,23 @@ checksum = "a3ee021e3a4c69d6ae90137fcf7537d1a8a5032dc9bf180c8fa6dd1a2f7c56d7" dependencies = [ "serde", "serde_json", - "wit-bindgen", + "wit-bindgen 0.41.0", ] [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", @@ -3568,15 +3519,15 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -3585,9 +3536,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -3596,9 +3547,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", @@ -3636,9 +3587,9 @@ dependencies = [ [[package]] name = "zopfli" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" dependencies = [ "bumpalo", "crc32fast", diff --git a/Cargo.toml b/Cargo.toml index 1dbf368..2056270 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,11 +64,11 @@ futures = "0.3.31" tree-sitter-python = "0.23.6" globwalk = "0.9.1" tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" [dependencies] ropey = { version = "2.0.0-beta.1", features = ["metric_chars"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" dashmap = { version = "6.1.0", features = ["raw-api"] } xmlparser = "0.13.5" pin-project-lite = "0.2.16" @@ -91,6 +91,8 @@ tree-sitter-javascript = "0.23.1" self_update = { version = "0.42.0", optional = true, default-features = false, features = ["archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate", "rustls"] } anyhow = { version = "1.0.97", features = ["backtrace"] } +serde.workspace = true +serde_json.workspace = true tree-sitter.workspace = true tree-sitter-python.workspace = true tokio.workspace = true diff --git a/src/analyze.rs b/src/analyze.rs index 6fcf835..bcbaf46 100644 --- a/src/analyze.rs +++ b/src/analyze.rs @@ -2,12 +2,12 @@ //! [`Index::model_of_range`] and [`Index::type_of`]. use std::{ - borrow::Cow, fmt::{Debug, Write}, ops::ControlFlow, - sync::{Arc, atomic::Ordering}, + sync::{Arc, OnceLock, RwLock, atomic::Ordering}, }; +use dashmap::DashMap; use fomat_macros::fomat; use lasso::Spur; use ropey::Rope; @@ -26,6 +26,23 @@ use ts_macros::query; mod scope; pub use scope::Scope; +pub fn type_cache() -> &'static TypeCache { + static CACHE: OnceLock = OnceLock::new(); + CACHE.get_or_init(TypeCache::default) +} + +macro_rules! _T { + (@ $builtin:expr) => { + $crate::analyze::type_cache().get_or_intern(Type::PyBuiltin($builtin.into())) + }; + ($model:literal) => { + $crate::analyze::type_cache().get_or_intern(Type::Model($model.into())) + }; + ($expr:expr) => { + $crate::analyze::type_cache().get_or_intern($expr) + }; +} + pub static MODEL_METHODS: phf::Set<&str> = phf::phf_set!( "create", "copy", @@ -51,7 +68,7 @@ pub static MODEL_METHODS: phf::Set<&str> = phf::phf_set!( ); /// The subset of types that may resolve to a model. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum Type { Env, /// \*.env.ref() @@ -64,36 +81,32 @@ pub enum Type { Super, Method(ModelName, ImStr), /// To hardcode some methods, such as dict.items() - PythonMethod(Box, ImStr), + PythonMethod(TypeId, ImStr), /// `odoo.http.request` HttpRequest, - Dict(Box<[Type; 2]>), + Dict(TypeId, TypeId), /// A bag of enumerated properties and their types - DictBag(Vec<(DictKey, Type)>), + DictBag(Vec<(DictKey, TypeId)>), /// Equivalent to Value, but may have a better semantic name PyBuiltin(ImStr), List(ListElement), - Tuple(Vec), - Iterable(Option>), + Tuple(Vec), + Iterable(Option), /// Can never be resolved, useful for non-model bindings. Value, } impl Type { - #[inline] - fn list_of(inner: Type) -> Self { - Type::List(ListElement::Occupied(Box::new(inner))) - } #[inline] fn is_dict(&self) -> bool { - matches!(self, Type::Dict(_)) + matches!(self, Type::Dict(..)) } } -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq, Hash)] pub enum ListElement { Vacant, - Occupied(Box), + Occupied(TypeId), } impl Debug for ListElement { @@ -105,20 +118,20 @@ impl Debug for ListElement { } } -impl From for Option { +impl From for Option { #[inline] fn from(value: ListElement) -> Self { match value { ListElement::Vacant => None, - ListElement::Occupied(inner) => Some(*inner), + ListElement::Occupied(inner) => Some(inner), } } } -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq, Hash)] pub enum DictKey { String(ImStr), - Type(Type), + Type(TypeId), } impl Debug for DictKey { @@ -156,6 +169,54 @@ impl core::fmt::Display for FunctionParam { } } +#[derive(Default)] +pub struct TypeCache { + types: RwLock>, + ids: DashMap, +} + +impl TypeCache { + #[inline] + pub fn get_or_intern(&self, type_: Type) -> TypeId { + if let Some(id) = self.ids.get(&type_) { + return *id; + } + self.intern(type_) + } + fn intern(&self, type_: Type) -> TypeId { + let mut types = self.types.write().unwrap(); + let id = TypeId(types.len() as u32); + types.push(type_.clone()); + self.ids.insert(type_, id); + id + } + #[inline] + pub fn resolve(&self, id: TypeId) -> Type { + self.types.read().unwrap()[id.0 as usize].clone() + } + pub fn is_dictlike(&self, id: TypeId) -> bool { + let types = self.types.read().unwrap(); + matches!( + &unsafe { types.get_unchecked(id.0 as usize) }, + Type::Dict(..) | Type::DictBag(..) + ) + } + pub fn is_dict(&self, id: TypeId) -> bool { + let types = self.types.read().unwrap(); + matches!(&unsafe { types.get_unchecked(id.0 as usize) }, Type::Dict(..)) + } +} + +#[repr(transparent)] +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct TypeId(u32); + +impl Debug for TypeId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + type_cache().resolve(*self).fmt(f) + } +} + pub fn normalize<'r, 'n>(node: &'r mut Node<'n>) -> &'r mut Node<'n> { let mut cursor = node.walk(); while matches!( @@ -234,9 +295,9 @@ impl Index { #[inline] pub fn model_of_range(&self, node: Node<'_>, range: ByteRange, contents: &str) -> Option { let (type_at_cursor, scope) = self.type_of_range(node, range, contents)?; - self.try_resolve_model(&type_at_cursor, &scope) + self.try_resolve_model(&type_cache().resolve(type_at_cursor), &scope) } - pub fn type_of_range(&self, root: Node<'_>, range: ByteRange, contents: &str) -> Option<(Type, Scope)> { + pub fn type_of_range(&self, root: Node<'_>, range: ByteRange, contents: &str) -> Option<(TypeId, Scope)> { // Phase 1: Determine the scope. let (self_type, fn_scope, self_param) = determine_scope(root, contents, range.start.0)?; @@ -266,7 +327,10 @@ impl Index { // TODO: fields if node_at_cursor.kind() == "identifier" && fn_scope.child_by_field_name("name") == Some(node_at_cursor) { return Some(( - Type::Method(_I(self_type).into(), contents[node_at_cursor.byte_range()].into()), + _T!(Type::Method( + _I(self_type).into(), + contents[node_at_cursor.byte_range()].into() + )), scope, )); } @@ -291,10 +355,10 @@ impl Index { let lhs = node.named_child(0).unwrap(); if lhs.kind() == "identifier" && let rhs = python_next_named_sibling(lhs).expect(format_loc!("rhs")) - && let Some(type_) = self.type_of(rhs, scope, contents) + && let Some(id) = self.type_of(rhs, scope, contents) { let lhs = &contents[lhs.byte_range()]; - scope.insert(lhs.to_string(), type_); + scope.insert(lhs.to_string(), type_cache().resolve(id)); } else if lhs.kind() == "subscript" && let Some(map) = dig!(lhs, identifier) && let Some(key) = dig!(lhs, string(1).string_content(1)) @@ -302,7 +366,7 @@ impl Index { && let type_ = self.type_of(rhs, scope, contents) && let Some(Type::DictBag(properties)) = scope.variables.get_mut(&contents[map.byte_range()]) { - let type_ = type_.unwrap_or(Type::Value); + let type_ = type_.unwrap_or_else(|| _T!(Type::Value)); let key = &contents[key.byte_range()]; if let Some(idx) = properties.iter().position(|(prop, _)| match prop { DictKey::String(prop) => prop.as_str() == key, @@ -316,7 +380,7 @@ impl Index { && let Some(rhs) = python_next_named_sibling(lhs) && let Some(type_) = self.type_of(rhs, scope, contents) { - destructure_into_patternlist_like(lhs, type_, scope, contents); + self.destructure_into_patternlist_like(lhs, type_, scope, contents); } } "for_statement" => { @@ -326,9 +390,9 @@ impl Index { if let Some(rhs) = python_next_named_sibling(lhs) && let Some(type_) = self.type_of(rhs, scope, contents) - && let Some(inner) = Self::type_of_iterable(type_) + && let Some(inner) = self.type_of_iterable(type_cache().resolve(type_)) { - destructure_into_patternlist_like(lhs, inner, scope, contents); + self.destructure_into_patternlist_like(lhs, inner, scope, contents); } return ControlFlow::Continue(true); } @@ -356,10 +420,10 @@ impl Index { let for_in = node.named_child(1).unwrap(); if let Some(lhs) = for_in.child_by_field_name("left") && let Some(rhs) = for_in.child_by_field_name("right") - && let Some(type_) = self.type_of(rhs, scope, contents) - && let Some(inner) = Self::type_of_iterable(type_) + && let Some(tid) = self.type_of(rhs, scope, contents) + && let Some(inner) = self.type_of_iterable(type_cache().resolve(tid)) { - destructure_into_patternlist_like(lhs, inner, scope, contents); + self.destructure_into_patternlist_like(lhs, inner, scope, contents); } } "call" if node.byte_range().contains_end(offset) => { @@ -371,14 +435,14 @@ impl Index { && let callee = mapped_call .nodes_for_capture_index(MappedCall::Callee as _) .next() - .unwrap() && let Some(type_) = self.type_of(callee, scope, contents) + .unwrap() && let Some(tid) = self.type_of(callee, scope, contents) { let iter = mapped_call .nodes_for_capture_index(MappedCall::Iter as _) .next() .unwrap(); let iter = &contents[iter.byte_range()]; - scope.insert(iter.to_string(), type_); + scope.insert(iter.to_string(), type_cache().resolve(tid)); } } "call" => { @@ -389,13 +453,13 @@ impl Index { return ControlFlow::Continue(false); }; if let Some(value) = call.nodes_for_capture_index(PythonBuiltinCall::AppendValue as _).next() - && let Some(value) = self.type_of(value, scope, contents) + && let Some(tid) = self.type_of(value, scope, contents) { if let Some(list) = call.nodes_for_capture_index(PythonBuiltinCall::AppendList as _).next() { if let Some(Type::List(slot @ ListElement::Vacant)) = scope.variables.get_mut(&contents[list.byte_range()]) { - *slot = ListElement::Occupied(Box::new(value)); + *slot = ListElement::Occupied(tid); } } else if let Some(map) = call.nodes_for_capture_index(PythonBuiltinCall::AppendMap as _).next() { let Some(key) = call @@ -407,12 +471,13 @@ impl Index { let key = &contents[key.byte_range()]; if let Some(Type::DictBag(properties)) = scope.variables.get_mut(&contents[map.byte_range()]) - && let Some((_, Type::List(slot @ ListElement::Vacant))) = - properties.iter_mut().find(|(prop, _)| match prop { - DictKey::String(prop) => prop.as_str() == key, - DictKey::Type(_) => false, - }) { - *slot = ListElement::Occupied(Box::new(value)); + && let Some((_, slot)) = properties.iter_mut().find(|(prop, id)| match prop { + DictKey::String(prop) => { + prop.as_str() == key && _T!(Type::List(ListElement::Vacant)) == *id + } + DictKey::Type(_) => false, + }) { + *slot = _T!(Type::List(ListElement::Occupied(tid))); } } } else if let Some(map) = call.nodes_for_capture_index(PythonBuiltinCall::UpdateMap as _).next() { @@ -427,7 +492,8 @@ impl Index { let mut cursor = args.walk(); let mut children = args.named_children(&mut cursor); if let Some(first) = children.by_ref().next() - && let Some(Type::DictBag(update_props)) = self.type_of(first, scope, contents) + && let Some(tid) = self.type_of(first, scope, contents) + && let Type::DictBag(update_props) = type_cache().resolve(tid) { properties.extend(update_props); } @@ -438,7 +504,7 @@ impl Index { && let Some(value) = named_arg.child_by_field_name("value") { let key = &contents[name.byte_range()]; - let type_ = self.type_of(value, scope, contents).unwrap_or(Type::Value); + let type_ = self.type_of(value, scope, contents).unwrap_or_else(|| _T!(Type::Value)); if let Some(idx) = properties.iter().position(|(prop, _)| match prop { DictKey::String(prop) => prop.as_str() == key, DictKey::Type(_) => false, @@ -449,7 +515,8 @@ impl Index { } } else if named_arg.kind() == "dictionary_splat" && let Some(value) = named_arg.named_child(0) - && let Some(Type::DictBag(update_props)) = self.type_of(value, scope, contents) + && let Some(tid) = self.type_of(value, scope, contents) + && let Type::DictBag(update_props) = type_cache().resolve(tid) { properties.extend(update_props); } @@ -482,10 +549,10 @@ impl Index { && let Some(type_) = self.type_of(first_arg, scope, contents) { let alias = &contents[alias.byte_range()]; - scope.insert(alias.to_string(), type_); + scope.insert(alias.to_string(), type_cache().resolve(type_)); } else if let Some(type_) = self.type_of(value, scope, contents) { let alias = &contents[alias.byte_range()]; - scope.insert(alias.to_string(), type_); + scope.insert(alias.to_string(), type_cache().resolve(type_)); } } } @@ -495,7 +562,7 @@ impl Index { ControlFlow::Continue(false) } /// [Type::Value] is not returned by this method. - pub fn type_of(&self, mut node: Node, scope: &Scope, contents: &str) -> Option { + pub fn type_of(&self, mut node: Node, scope: &Scope, contents: &str) -> Option { // What contributes to value types? // 1. *.env['foo'] => Model('foo') // 2. *.env.ref() => Model() @@ -530,17 +597,16 @@ impl Index { let lhs = node.child_by_field_name("value")?; let rhs = node.child_by_field_name("subscript")?; let obj_ty = self.type_of(lhs, scope, contents)?; - match obj_ty { + match type_cache().resolve(obj_ty) { Type::Env if rhs.kind() == "string" => { - Some(Type::Model(contents[rhs.byte_range().shrink(1)].into())) + Some(_T!(Type::Model(contents[rhs.byte_range().shrink(1)].into()))) } - Type::Env => Some(Type::Model(ImStr::from_static("_unknown"))), + Type::Env => Some(_T!["unknown"]), Type::Model(_) | Type::Record(_) => Some(obj_ty), - Type::Dict(dict) => { - let [key, value] = *dict; + Type::Dict(key, value) => { let rhs = self.type_of(rhs, scope, contents); // FIXME: We trust that the user makes the correct judgment here and returns the type requested. - rhs.is_none_or(|lhs| lhs == key).then_some(value) + rhs.is_none_or(|rhs| rhs == key).then_some(value) } Type::DictBag(properties) => { // compare by key @@ -569,7 +635,7 @@ impl Index { None } // FIXME: Again, just trust that the user is doing the right thing. - Type::List(ListElement::Occupied(slot)) => Some(*slot), + Type::List(ListElement::Occupied(slot)) => Some(slot), _ => None, } } @@ -584,13 +650,13 @@ impl Index { let key = &contents[node.byte_range()]; if key == "super" { - return Some(Type::Super); + return Some(_T!(Type::Super)); } if let Some(type_) = scope.get(key) { - return Some(type_.clone()); + return Some(_T!(type_.clone())); } if key == "request" { - return Some(Type::HttpRequest); + return Some(_T!(Type::HttpRequest)); } None } @@ -625,11 +691,11 @@ impl Index { && let Some(scrutinee) = for_in_clause.child_by_field_name("left") && let Some(iteratee) = for_in_clause.child_by_field_name("right") && let Some(iter_ty) = self.type_of(iteratee, scope, contents) - && let Some(iter_ty) = Self::type_of_iterable(iter_ty) + && let Some(iter_ty) = self.type_of_iterable(type_cache().resolve(iter_ty)) { // FIXME: How to prevent this clone? comprehension_scope = Scope::new(Some(scope.clone())); - destructure_into_patternlist_like(scrutinee, iter_ty, &mut comprehension_scope, contents); + self.destructure_into_patternlist_like(scrutinee, iter_ty, &mut comprehension_scope, contents); pair_scope = &comprehension_scope; } let lhs = pair @@ -639,10 +705,8 @@ impl Index { .named_child(1) .and_then(|lhs| self.type_of(lhs, pair_scope, contents)); if lhs.is_some() || rhs.is_some() { - Some(Type::Dict(Box::new([ - lhs.unwrap_or(Type::Value), - rhs.unwrap_or(Type::Value), - ]))) + let value_id = _T!(Type::Value); + Some(_T!(Type::Dict(lhs.unwrap_or(value_id), rhs.unwrap_or(value_id)))) } else { None } @@ -658,51 +722,52 @@ impl Index { if let Some(lhs) = dig!(lhs, string_content(1)) { key = DictKey::String(ImStr::from(&contents[lhs.byte_range()])); } else if matches!(lhs.kind(), "true" | "false" | "string" | "none" | "float" | "integer") { - key = DictKey::Type(Type::PyBuiltin(ImStr::from(&contents[lhs.byte_range()]))); + key = DictKey::Type(_T!( @contents[lhs.byte_range()])); } else if let Some(lhs) = self.type_of(lhs, scope, contents) { key = DictKey::Type(lhs); } else { continue; } - let value = self.type_of(rhs, scope, contents).unwrap_or(Type::Value); + let value = self.type_of(rhs, scope, contents).unwrap_or_else(|| _T!(Type::Value)); properties.push((key, value)); } } - Some(Type::DictBag(properties)) + Some(_T!(Type::DictBag(properties))) } "list" => { let mut slot = ListElement::Vacant; for child in node.named_children(&mut node.walk()) { if let Some(child) = self.type_of(child, scope, contents) { - slot = ListElement::Occupied(Box::new(child)); + slot = ListElement::Occupied(child); break; } } - Some(Type::List(slot)) + Some(_T!(Type::List(slot))) } "expression_list" | "tuple" => { let mut cursor = node.walk(); + let value_id = _T!(Type::Value); let tuple = node.named_children(&mut cursor).filter_map(|child| { if child.kind() == "comment" { return None; } - Some(self.type_of(child, scope, contents).unwrap_or(Type::Value)) + Some(self.type_of(child, scope, contents).unwrap_or(value_id)) }); - Some(Type::Tuple(tuple.collect())) + Some(_T!(Type::Tuple(tuple.collect()))) } - "string" => Some(Type::PyBuiltin(ImStr::from_static("str"))), - "integer" => Some(Type::PyBuiltin(ImStr::from_static("int"))), - "float" => Some(Type::PyBuiltin(ImStr::from_static("float"))), - "true" | "false" | "comparison_operator" => Some(Type::PyBuiltin(ImStr::from_static("bool"))), + "string" => Some(_T!( @ "str")), + "integer" => Some(_T!( @ "int")), + "float" => Some(_T!( @ "float")), + "true" | "false" | "comparison_operator" => Some(_T!( @ "bool")), _ => None, } } - fn type_of_iterable(type_: Type) -> Option { + fn type_of_iterable(&self, type_: Type) -> Option { match type_ { - Type::Model(_) => Some(type_), + Type::Model(_) => Some(_T!(type_)), Type::List(inner) => inner.into(), - Type::Iterable(inner) => inner.map(|inner| *inner), + Type::Iterable(inner) => inner, // TODO: tuple -> union _ => None, } @@ -713,37 +778,39 @@ impl Index { _ => producer(type_), } } - fn type_of_call_node(&self, call: Node<'_>, scope: &Scope, contents: &str) -> Option { + fn type_of_call_node(&self, call: Node<'_>, scope: &Scope, contents: &str) -> Option { let func = call.named_child(0)?; if func.kind() == "identifier" { match &contents[func.byte_range()] { "zip" => { let args = call.named_child(1)?; let mut cursor = args.walk(); + let value_id = _T!(Type::Value); let children = args.named_children(&mut cursor).map(|child| { - Self::type_of_iterable(self.type_of(child, scope, contents).unwrap_or(Type::Value)) - .unwrap_or(Type::Value) + let type_ = type_cache().resolve(self.type_of(child, scope, contents).unwrap_or(value_id)); + self.type_of_iterable(type_).unwrap_or(value_id) }); - return Some(Type::Iterable(Some(Box::new(Type::Tuple(children.collect()))))); + let tuple = _T!(Type::Tuple(children.collect())); + return Some(_T!(Type::Iterable(Some(tuple)))); } "enumerate" => { let arg = call.named_child(1)?.named_child(0); let arg = arg .and_then(|arg| self.type_of(arg, scope, contents)) - .unwrap_or(Type::Value); - return Some(Type::Iterable(Some(Box::new(Type::Tuple(vec![ - Type::PyBuiltin(ImStr::from_static("int")), - arg, - ]))))); + .unwrap_or_else(|| _T!(Type::Value)); + let intid = _T!(Type::PyBuiltin("int".into())); + let tuple = _T!(Type::Tuple(vec![intid, arg])); + return Some(_T!(Type::Iterable(Some(tuple)))); } "tuple" => { let args = call.named_child(1)?; if args.kind() == "argument_list" { let mut cursor = args.walk(); + let value_id = _T!(Type::Value); let children = args .named_children(&mut cursor) - .map(|child| self.type_of(child, scope, contents).unwrap_or(Type::Value)); - return Some(Type::Tuple(children.collect())); + .map(|child| self.type_of(child, scope, contents).unwrap_or(value_id)); + return Some(_T!(Type::Tuple(children.collect()))); } } "super" => {} @@ -752,18 +819,18 @@ impl Index { } let func = self.type_of(func, scope, contents)?; - match func { + match type_cache().resolve(func) { Type::RefFn => { // (call (_) @func (argument_list . (string) @xml_id)) let xml_id = call.named_child(1)?.named_child(0)?; if xml_id.kind() == "string" { - Some(Type::Record(contents[xml_id.byte_range().shrink(1)].into())) + Some(_T!(Type::Record(contents[xml_id.byte_range().shrink(1)].into()))) } else { None } } - Type::ModelFn(model) => Some(Type::Model(model)), - Type::Super => scope.get(scope.super_.as_deref()?).cloned(), + Type::ModelFn(model) => Some(_T!(Type::Model(model))), + Type::Super => Some(_T!(scope.get(scope.super_.as_deref()?).cloned()?)), Type::Method(model, mapped) if mapped == "mapped" => { // (call (_) @func (argument_list . [(string) (lambda)] @mapped)) let mapped = call.named_child(1)?.named_child(0)?; @@ -772,7 +839,8 @@ impl Index { let mut model: Spur = model.into(); let mut mapped = &contents[mapped.byte_range().shrink(1)]; self.models.resolve_mapped(&mut model, &mut mapped, None).ok()?; - self.type_of_attribute(&Type::Model(ImStr::from_static(_R(model))), mapped, scope) + self.type_of_attribute(&Type::Model(_R(model).into()), mapped, scope) + .map(|it| _T!(it)) } "lambda" => { // (lambda (lambda_parameters)? body: (_)) @@ -781,12 +849,15 @@ impl Index { let first_arg = params.named_child(0)?; if first_arg.kind() == "identifier" { let first_arg = &contents[first_arg.byte_range()]; - scope.insert(first_arg.to_string(), Type::Model(ImStr::from_static(_R(model)))); + scope.insert(first_arg.to_string(), Type::Model(_R(model).into())); } } let body = mapped.child_by_field_name(b"body")?; - let type_ = self.type_of(body, &scope, contents).unwrap_or(Type::Value); - Some(Index::wrap_in_container(type_, Type::list_of)) + let type_ = self.type_of(body, &scope, contents).unwrap_or_else(|| _T!(Type::Value)); + let type_ = Index::wrap_in_container(type_cache().resolve(type_), |it| { + Type::List(ListElement::Occupied(_T!(it))) + }); + Some(_T!(type_)) } _ => None, } @@ -799,9 +870,9 @@ impl Index { let mut model: Spur = model.into(); let mut grouped = &contents[grouped.byte_range().shrink(1)]; self.models.resolve_mapped(&mut model, &mut grouped, None).ok()?; - let model = Type::Model(ImStr::from_static(_R(model))); + let model = Type::Model(_R(model).into()); let groupby = self.type_of_attribute(&model, grouped, scope)?; - Some(Type::Dict(Box::new([groupby, model]))) + Some(_T!(Type::Dict(_T!(groupby), _T!(model)))) } "lambda" => { let mut scope = Scope::new(Some(scope.clone())); @@ -809,13 +880,13 @@ impl Index { let first_arg = params.named_child(0)?; if first_arg.kind() == "identifier" { let first_arg = &contents[first_arg.byte_range()]; - scope.insert(first_arg.to_string(), Type::Model(ImStr::from_static(_R(model)))); + scope.insert(first_arg.to_string(), Type::Model(_R(model).into())); } } let body = grouped.child_by_field_name(b"body")?; - let groupby = self.type_of(body, &scope, contents).unwrap_or(Type::Value); - let model = Type::Model(ImStr::from_static(_R(model))); - Some(Type::Dict(Box::new([groupby, model]))) + let groupby = self.type_of(body, &scope, contents).unwrap_or_else(|| _T!(Type::Value)); + let model = Type::Model(_R(model).into()); + Some(_T!(Type::Dict(groupby, _T!(model)))) } _ => None, } @@ -865,21 +936,29 @@ impl Index { groupby.extend(aggs); groupby.dedup(); - let model = Type::Model(ImStr::from_static(_R(model))); + let model = Type::Model(_R(model).into()); + let value_id = _T!(Type::Value); // FIXME: This is not quite correct as only recordset and numeric aggregations make sense. let aggs = groupby .into_iter() - .map(|attr| self.type_of_attribute(&model, attr, scope).unwrap_or(Type::Value)); - Some(Type::List(ListElement::Occupied(Box::new(Type::Tuple(aggs.collect()))))) + .map(|attr| match self.type_of_attribute(&model, attr, scope) { + Some(type_) => _T!(type_), + None => value_id, + }); + let tuple = _T!(Type::Tuple(aggs.collect())); + Some(_T!(Type::List(ListElement::Occupied(tuple)))) } Type::Method(model, method) => { let method = _G(&method)?; let args = self.prepare_call_scope(model, method.into(), call, scope, contents); - self.eval_method_rtype(method.into(), *model, args) + Some(self.eval_method_rtype(method.into(), *model, args)?) } - Type::PythonMethod(dict, items) if dict.is_dict() && items == "items" => { - let Type::Dict(dict) = *dict else { unreachable!() }; - Some(Type::Iterable(Some(Box::new(Type::Tuple(dict.to_vec()))))) + Type::PythonMethod(dict, items) if type_cache().is_dict(dict) && items == "items" => { + let Type::Dict(lhs, rhs) = type_cache().resolve(dict) else { + unreachable!() + }; + let tuple = _T!(Type::Tuple(vec![lhs, rhs])); + Some(_T!(Type::Iterable(Some(tuple)))) } Type::Env | Type::Record(..) @@ -904,7 +983,7 @@ impl Index { call: Node, scope: &Scope, contents: &str, - ) -> Option { + ) -> Option<(Vec, Scope)> { // (call // (arguments_list // (_) @@ -919,7 +998,8 @@ impl Index { } drop(model); - let mut out = Scope::new(None); + let mut argtypes = Scope::new(None); + let mut args = vec![]; for (idx, arg) in arguments_list.named_children(&mut arguments_list.walk()).enumerate() { if arg.kind() == "keyword_argument" && let Some(key) = arg.child_by_field_name("key") @@ -932,37 +1012,40 @@ impl Index { }) { continue; } - let Some(value) = self.type_of(value, scope, contents) else { + let Some(tid) = self.type_of(value, scope, contents) else { continue; }; - out.insert(key.to_string(), value); + args.push(key.into()); + argtypes.insert(key.to_string(), type_cache().resolve(tid)); } else if let Some(FunctionParam::Param(argname)) = arguments.get(idx) - && let Some(value) = self.type_of(arg, scope, contents) + && let Some(tid) = self.type_of(arg, scope, contents) { - out.insert(argname.to_string(), value); + args.push(argname.clone()); + argtypes.insert(argname.to_string(), type_cache().resolve(tid)); } else { continue; } } - Some(out) + Some((args, argtypes)) } #[instrument(skip_all, ret)] - fn type_of_attribute_node(&self, attribute: Node<'_>, scope: &Scope, contents: &str) -> Option { + fn type_of_attribute_node(&self, attribute: Node<'_>, scope: &Scope, contents: &str) -> Option { let lhs = attribute.named_child(0)?; - let lhs = self.type_of(lhs, scope, contents)?; + let lhsid = self.type_of(lhs, scope, contents)?; + let lhs = type_cache().resolve(lhsid); let rhs = attribute.named_child(1)?; let attrname = &contents[rhs.byte_range()]; match &contents[rhs.byte_range()] { "env" if matches!(lhs, Type::Model(..) | Type::Record(..) | Type::HttpRequest) => Some(Type::Env), "ref" if matches!(lhs, Type::Env) => Some(Type::RefFn), - "user" if matches!(lhs, Type::Env) => Some(Type::Model(ImStr::from_static("res.users"))), - "company" | "companies" if matches!(lhs, Type::Env) => Some(Type::Model(ImStr::from_static("res.company"))), + "user" if matches!(lhs, Type::Env) => Some(Type::Model("res.users".into())), + "company" | "companies" if matches!(lhs, Type::Env) => Some(Type::Model("res.company".into())), "mapped" | "grouped" | "_read_group" => { let model = self.try_resolve_model(&lhs, scope)?; Some(Type::Method(model, attrname.into())) } - dict_method @ "items" if lhs.is_dict() => Some(Type::PythonMethod(Box::new(lhs), dict_method.into())), + dict_method @ "items" if lhs.is_dict() => Some(Type::PythonMethod(lhsid, dict_method.into())), func if MODEL_METHODS.contains(func) => match lhs { Type::Model(model) => Some(Type::ModelFn(model)), Type::Record(xml_id) => { @@ -975,6 +1058,7 @@ impl Index { ident if rhs.kind() == "identifier" => self.type_of_attribute(&lhs, ident, scope), _ => None, } + .map(|it| _T!(it)) } #[instrument(skip_all, fields(attr=attr), ret)] pub fn type_of_attribute(&self, type_: &Type, attr: &str, scope: &Scope) -> Option { @@ -991,11 +1075,11 @@ impl Index { } match _R(type_) { - "Selection" | "Char" | "Text" | "Html" => Some(Type::PyBuiltin(ImStr::from_static("str"))), - "Integer" => Some(Type::PyBuiltin(ImStr::from_static("int"))), - "Float" | "Monetary" => Some(Type::PyBuiltin(ImStr::from_static("float"))), - "Date" => Some(Type::PyBuiltin(ImStr::from_static("date"))), - "Datetime" => Some(Type::PyBuiltin(ImStr::from_static("datetime"))), + "Selection" | "Char" | "Text" | "Html" => Some(Type::PyBuiltin("str".into())), + "Integer" => Some(Type::PyBuiltin("int".into())), + "Float" | "Monetary" => Some(Type::PyBuiltin("float".into())), + "Date" => Some(Type::PyBuiltin("date".into())), + "Datetime" => Some(Type::PyBuiltin("datetime".into())), _ => None, } } @@ -1003,25 +1087,22 @@ impl Index { } } else { match attr { - "id" if matches!(type_, Type::Model(..) | Type::Record(..)) => { - Some(Type::PyBuiltin(ImStr::from_static("int"))) + "id" if matches!(type_, Type::Model(..) | Type::Record(..)) => Some(Type::PyBuiltin("int".into())), + "ids" if matches!(type_, Type::Model(..) | Type::Record(..)) => { + Some(Type::List(ListElement::Occupied(_T!(Type::PyBuiltin("int".into()))))) } - "ids" if matches!(type_, Type::Model(..) | Type::Record(..)) => Some(Type::List( - ListElement::Occupied(Box::new(Type::PyBuiltin(ImStr::from_static("int")))), - )), "display_name" if matches!(type_, Type::Model(..) | Type::Record(..)) => { - Some(Type::PyBuiltin(ImStr::from_static("str"))) + Some(Type::PyBuiltin("str".into())) } "create_date" | "write_date" if matches!(type_, Type::Model(..) | Type::Record(..)) => { - Some(Type::PyBuiltin(ImStr::from_static("datetime"))) + Some(Type::PyBuiltin("datetime".into())) } "create_uid" | "write_uid" if matches!(type_, Type::Model(..) | Type::Record(..)) => { - Some(Type::Model(ImStr::from_static("res.users"))) + Some(Type::Model("res.users".into())) + } + "_fields" if matches!(type_, Type::Model(..) | Type::Record(..)) => { + Some(Type::Dict(_T!(Type::PyBuiltin("str".into())), _T!["ir.model.fields"])) } - "_fields" if matches!(type_, Type::Model(..) | Type::Record(..)) => Some(Type::Dict(Box::new([ - Type::PyBuiltin(ImStr::from_static("str")), - Type::Model(ImStr::from_static("ir.model.fields")), - ]))), "env" if matches!(type_, Type::Model(..) | Type::Record(..) | Type::HttpRequest) => Some(Type::Env), _ => None, } @@ -1051,42 +1132,39 @@ impl Index { } } #[inline] - pub fn type_display<'a>(&self, type_: &'a Type) -> Option> { + pub fn type_display(&self, type_: TypeId) -> Option { self.type_display_indent(type_, 0) } - fn type_display_indent<'a>(&self, type_: &'a Type, indent: usize) -> Option> { - match type_ { - Type::Dict(pair) => { - let [lhs, rhs] = &**pair; + fn type_display_indent(&self, type_: TypeId, indent: usize) -> Option { + match type_cache().resolve(type_) { + Type::Dict(lhs, rhs) => { let lhs = self.type_display_indent(lhs, indent); let lhs = lhs.as_deref().unwrap_or("..."); let rhs = self.type_display_indent(rhs, indent); let rhs = rhs.as_deref().unwrap_or("..."); - Some(fomat! { "dict[" (lhs) ", " (rhs) "]" }.into()) + Some(fomat! { "dict[" (lhs) ", " (rhs) "]" }) } Type::DictBag(properties) => { let preindent = " ".repeat(indent + 2); + let empty_properties = properties.is_empty(); let properties_fragment = fomat! { for (key, value) in properties { (preindent) match key { DictKey::String(key) => { "\"" (key) "\"" } - DictKey::Type(Type::Dict(..) | Type::DictBag(..)) => { "{...}" } + DictKey::Type(key) if type_cache().is_dictlike(key) => { "{...}" } DictKey::Type(key) => { (self.type_display_indent(key, indent + 2).as_deref().unwrap_or("...")) } } ": " (self.type_display_indent(value, indent + 2).as_deref().unwrap_or("...")) } sep { ",\n" } }; let unindent = " ".repeat(indent); - Some( - fomat! { - if !properties.is_empty() { - "{\n" (properties_fragment) "\n" (unindent) "}" - } else { - "{}" - } + Some(fomat! { + if !empty_properties { + "{\n" (properties_fragment) "\n" (unindent) "}" + } else { + "{}" } - .into(), - ) + }) } Type::PyBuiltin(builtin) => Some(builtin.as_str().into()), Type::List(slot) => { @@ -1095,38 +1173,33 @@ impl Index { ListElement::Occupied(slot) => self.type_display_indent(slot, indent), }; Some(match slot { - Some(slot) => format!("list[{slot}]").into(), + Some(slot) => format!("list[{slot}]"), None => "list".into(), }) } Type::Env => Some("Environment".into()), - Type::Model(model) => Some(format!(r#"Model["{model}"]"#).into()), + Type::Model(model) => Some(format!(r#"Model["{model}"]"#)), Type::Record(xml_id) => { let xml_id = _G(xml_id)?; let record = self.records.get(&xml_id.into())?; Some(_R(record.model?).into()) } - Type::Tuple(items) => Some( - fomat! { - "tuple[" - for item in items { - (self.type_display_indent(item, indent).as_deref().unwrap_or("...")) - } sep { ", " } - "]" - } - .into(), - ), + Type::Tuple(items) => Some(fomat! { + "tuple[" + for item in items { + (self.type_display_indent(item, indent).as_deref().unwrap_or("...")) + } sep { ", " } + "]" + }), Type::Iterable(output) => { - let output = output - .as_deref() - .and_then(|inner| self.type_display_indent(inner, indent)); + let output = output.and_then(|inner| self.type_display_indent(inner, indent)); let output = output.as_deref().unwrap_or("..."); - Some(format!("Iterable[{output}]").into()) + Some(format!("Iterable[{output}]")) } Type::Method(..) => unreachable!("Bug: this function should not handle methods"), Type::RefFn | Type::ModelFn(_) | Type::Super | Type::HttpRequest | Type::Value | Type::PythonMethod(..) => { if cfg!(debug_assertions) { - Some(format!("{type_:?}").into()) + Some(format!("{type_:?}")) } else { None } @@ -1168,11 +1241,16 @@ impl Index { } /// Resolves the return type of a method as well as populating its arguments and docstring. /// - /// `parameters` can be provided using [`Index::prepare_call_scope`]. + /// `parameters` can be provided using [`Index::prepare_call_scope`]. #[instrument(level = "trace", ret, skip(self, model), fields(model = _R(model)))] - pub fn eval_method_rtype(&self, method: Symbol, model: Spur, parameters: Option) -> Option { + pub fn eval_method_rtype( + &self, + method: Symbol, + model: Spur, + parameters: Option<(Vec, Scope)>, + ) -> Option { _ = self.models.populate_properties(model.into(), &[]); - let mut model_entry = self.models.get_mut(&model.into())?; + let mut model_entry = self.models.try_get_mut(&model.into()).expect(format_loc!("deadlock"))?; let method_obj = model_entry.methods.as_mut()?.get_mut(&method)?; if method_obj @@ -1192,6 +1270,16 @@ impl Index { } })); + let (argnames, mut scope) = parameters.unwrap_or_default(); + let cache_key = argnames + .into_iter() + .map(|arg| _T!(scope.variables.get(&*arg).cloned().unwrap_or(Type::Value))) + .collect::>(); + if let Some(tid) = method_obj.eval_cache.get(&cache_key) { + drop(model_entry); + return Some(tid); + } + let location = method_obj.locations.first().cloned()?; drop(model_entry); @@ -1243,7 +1331,6 @@ impl Index { } let (self_type, fn_scope, self_param) = determine_scope(ast.root_node(), &contents, end_offset.0)?; - let mut scope = parameters.unwrap_or_default(); let self_type = match self_type { Some(type_) => &contents[type_.byte_range().shrink(1)], None => "", @@ -1262,6 +1349,7 @@ impl Index { return ControlFlow::Continue(entered); }; + let type_ = type_cache().resolve(type_); return match self.try_resolve_model(&type_, scope) { Some(resolved) => ControlFlow::Break(Some(Type::Model(ImStr::from(_R(resolved))))), None => ControlFlow::Break(Some(type_)), @@ -1307,7 +1395,38 @@ impl Index { } method.pending_eval.store(false, Ordering::Release); - type_ + if let Some(type_) = type_ { + let tid = _T!(type_); + method.eval_cache.insert(cache_key, tid); + Some(tid) + } else { + None + } + } + /// `pattern` is `(identifier | pattern_list | tuple_pattern)`, the `a, b` in `for a, b in ...`. + fn destructure_into_patternlist_like(&self, pattern: Node, tid: TypeId, scope: &mut Scope, contents: &str) { + if pattern.kind() == "identifier" { + let name = &contents[pattern.byte_range()]; + scope.insert(name.to_string(), type_cache().resolve(tid)); + } else if matches!(pattern.kind(), "pattern_list" | "tuple_pattern") { + if let Type::Tuple(mut inner) = type_cache().resolve(tid) { + inner.reverse(); + for child in pattern.named_children(&mut pattern.walk()) { + if matches!(child.kind(), "identifier" | "tuple_pattern") + && let Some(type_) = inner.pop() + { + self.destructure_into_patternlist_like(child, type_, scope, contents); + } + } + } else if let Some(inner) = self.type_of_iterable(type_cache().resolve(tid)) { + // spread this type to all params + for child in pattern.named_children(&mut pattern.walk()) { + if matches!(child.kind(), "identifier" | "tuple_pattern") { + self.destructure_into_patternlist_like(child, inner, scope, contents); + } + } + } + } } fn parse_method_docstring<'out>(fn_scope: Node, contents: &'out str) -> Option<&'out str> { let block = fn_scope.child_by_field_name("body")?; @@ -1364,32 +1483,6 @@ pub fn determine_scope<'out, 'node>( Some((self_type, fn_scope, self_param)) } -/// `pattern` is `(identifier | pattern_list | tuple_pattern)`, the `a, b` in `for a, b in ...`. -fn destructure_into_patternlist_like(pattern: Node, type_: Type, scope: &mut Scope, contents: &str) { - if pattern.kind() == "identifier" { - let name = &contents[pattern.byte_range()]; - scope.insert(name.to_string(), type_); - } else if matches!(pattern.kind(), "pattern_list" | "tuple_pattern") { - if let Type::Tuple(mut inner) = type_ { - inner.reverse(); - for child in pattern.named_children(&mut pattern.walk()) { - if matches!(child.kind(), "identifier" | "tuple_pattern") - && let Some(type_) = inner.pop() - { - destructure_into_patternlist_like(child, type_, scope, contents); - } - } - } else if let Some(inner) = Index::type_of_iterable(type_) { - // spread this type to all params - for child in pattern.named_children(&mut pattern.walk()) { - if matches!(child.kind(), "identifier" | "tuple_pattern") { - destructure_into_patternlist_like(child, inner.clone(), scope, contents); - } - } - } - } -} - #[cfg(test)] mod tests { use pretty_assertions::assert_eq; @@ -1397,8 +1490,7 @@ mod tests { use tower_lsp_server::lsp_types::Position; use tree_sitter::{Parser, QueryCursor, StreamingIterator, StreamingIteratorMut}; - use crate::ImStr; - use crate::analyze::{FieldCompletion, Type}; + use crate::analyze::{FieldCompletion, Type, type_cache}; use crate::index::_I; use crate::utils::{ByteOffset, acc_vec, rope_conv}; use crate::{index::Index, test_utils::cases::foo::prepare_foo_index}; @@ -1475,7 +1567,7 @@ class Foo(models.Model): assert_eq!( index.eval_method_rtype(_I("test").into(), _I("bar"), None), - Some(Type::Model(ImStr::from_static("foo"))) + Some(type_cache().get_or_intern(Type::Model("foo".into()))) ) } @@ -1488,7 +1580,7 @@ class Foo(models.Model): assert_eq!( index.eval_method_rtype(_I("test").into(), _I("quux"), None), - Some(Type::Model(ImStr::from_static("foo"))) + Some(type_cache().get_or_intern(Type::Model("foo".into()))) ) } } diff --git a/src/backend.rs b/src/backend.rs index 921ffec..ade88b9 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -19,7 +19,7 @@ use tower_lsp_server::Client; use tower_lsp_server::{UriExt, lsp_types::*}; use tree_sitter::{Parser, Tree}; -use crate::analyze::{Scope, Type}; +use crate::analyze::{Scope, Type, TypeId, type_cache}; use crate::prelude::*; use crate::component::{Prop, PropDescriptor}; @@ -98,6 +98,7 @@ pub struct Capabilities { pub pull_diagnostics: AtomicBool, /// Whether [`workspace/workspaceFolders`][request::WorkspaceFoldersRequest] can be called. pub workspace_folders: AtomicBool, + pub can_create_wdp: AtomicBool, } pub struct TextDocumentItem { @@ -775,7 +776,7 @@ impl Index { let model_key = _G(&model)?; let method_name = _R(method); let rtype = self.eval_method_rtype(method.into(), model_key, None); - let rtype = rtype.as_ref().and_then(|rtype| self.type_display(rtype)); + let rtype = rtype.and_then(|rtype| self.type_display(rtype)); let entry = self.models.get(&model_key.into())?; let methods = entry.methods.as_ref()?; let method_entry = methods.get(&method.into())?; @@ -838,12 +839,12 @@ impl Index { pub fn hover_variable( &self, name: Option<&str>, - type_: Type, + type_: TypeId, range: Option, ) -> anyhow::Result> { - let type_fragment = match type_ { + let type_fragment = match type_cache().resolve(type_) { Type::Method(model, method) => return self.hover_property_name(&method, _R(model), range), - _ => self.type_display(&type_), + _ => self.type_display(type_), }; let type_fragment = type_fragment.as_deref().unwrap_or("Unknown"); let value = fomat! { @@ -932,7 +933,7 @@ impl Index { { drop(entry); let rtype = self.eval_method_rtype(prop.into(), model_key, None); - let rtype = rtype.as_ref().and_then(|rtype| self.type_display(rtype)); + let rtype = rtype.and_then(|rtype| self.type_display(rtype)); let model = self.models.get(&model_key.into()).unwrap(); let method = model.methods.as_ref().unwrap().get(&prop.into()).unwrap(); Ok(Some(Hover { @@ -945,7 +946,7 @@ impl Index { } else { drop(entry); let attr_type = some!(self.type_of_attribute(&Type::Model(ImStr::from(model)), name, &Scope::new(None))); - self.hover_variable(Some(name), attr_type, range) + self.hover_variable(Some(name), type_cache().get_or_intern(attr_type), range) } } /// Returns a Markdown-formatted docstring for a model. diff --git a/src/model.rs b/src/model.rs index 8a9403f..7358b4e 100644 --- a/src/model.rs +++ b/src/model.rs @@ -13,11 +13,13 @@ use dashmap::DashMap; use dashmap::mapref::one::RefMut; use dashmap::try_result::TryResult; use derive_more::{Deref, DerefMut}; +use mini_moka::sync::Cache; use qp_trie::Trie; use rayon::prelude::{IntoParallelIterator, ParallelIterator}; use smart_default::SmartDefault; use ts_macros::query; +use crate::analyze::TypeId; use crate::prelude::*; use crate::analyze::FunctionParam; @@ -102,12 +104,14 @@ pub struct Field { pub help: Option, } -#[derive(Debug)] +#[derive(Debug, SmartDefault)] pub struct Method { pub locations: Vec, pub docstring: Option, pub arguments: Option>, pub pending_eval: AtomicBool, + #[default(_code = "Cache::new(8)")] + pub eval_cache: Cache, TypeId>, } impl Clone for Method { @@ -117,6 +121,7 @@ impl Clone for Method { docstring: self.docstring.clone(), arguments: self.arguments.clone(), pending_eval: AtomicBool::new(false), + eval_cache: Cache::new(8), } } } @@ -692,9 +697,7 @@ impl ModelIndex { empty.insert( Method { locations: vec![method_location.into()], - docstring: None, - arguments: None, - pending_eval: AtomicBool::new(false), + ..Default::default() } .into(), ); @@ -864,7 +867,7 @@ impl ModelEntry { return Ok(()); } } - self.docstring = Some(ImStr::from_static("")); + self.docstring = Some("".into()); } Ok(()) diff --git a/src/python.rs b/src/python.rs index 5070f40..e8033af 100644 --- a/src/python.rs +++ b/src/python.rs @@ -10,7 +10,7 @@ use ts_macros::query; use crate::prelude::*; -use crate::analyze::Type; +use crate::analyze::{Type, type_cache}; use crate::index::{_G, _I, _R, PathSymbol, index_models}; use crate::model::{ModelName, ModelType}; use crate::xml::determine_csv_xmlid_subgroup; @@ -1119,7 +1119,7 @@ impl Backend { let lsp_range = span_conv(needle.range()); let (type_, scope) = some!((self.index).type_of_range(root, needle.byte_range().map_unit(ByteOffset), &contents)); - if let Some(model) = self.index.try_resolve_model(&type_, &scope) { + if let Some(model) = self.index.try_resolve_model(&type_cache().resolve(type_), &scope) { let model = _R(model); let identifier = (needle.kind() == "identifier").then(|| &contents[needle.byte_range()]); return self.index.hover_model(model, Some(lsp_range), true, identifier); @@ -1180,11 +1180,14 @@ impl Backend { }; let callee = some!(args.prev_named_sibling()); - let Some((Type::Method(model_key, method), _)) = + let Some((tid, _)) = (self.index).type_of_range(ast.root_node(), callee.byte_range().map_unit(ByteOffset), &contents) else { return Ok(None); }; + let Type::Method(model_key, method) = type_cache().resolve(tid) else { + return Ok(None); + }; let method_key = some!(_G(&method)); let rtype = (self.index).eval_method_rtype(method_key.into(), model_key.into(), None); let model = some!((self.index).models.get(&model_key)); @@ -1209,7 +1212,7 @@ impl Backend { }); } - let rtype = rtype.as_ref().and_then(|rtype| self.index.type_display(rtype)); + let rtype = rtype.and_then(|rtype| self.index.type_display(rtype)); match rtype { Some(rtype) => drop(write!(&mut label, ") -> {rtype}")), None => label.push_str(") -> ..."), diff --git a/src/python/completions.rs b/src/python/completions.rs index f88ca6b..c88b388 100644 --- a/src/python/completions.rs +++ b/src/python/completions.rs @@ -10,7 +10,7 @@ use tree_sitter::Tree; use crate::prelude::*; -use crate::analyze::{DictKey, Type}; +use crate::analyze::{DictKey, Type, type_cache}; use crate::backend::Backend; use crate::index::{_G, _I, _R, symbol::Symbol}; use crate::model::{FieldKind, ModelEntry, ModelName, PropertyKind}; @@ -90,7 +90,7 @@ impl Backend { &needle, range.map_unit(ByteOffset), rope, - model_filter.map(|m| vec![ImStr::from_static(m)]).as_deref(), + model_filter.map(|m| vec![m.into()]).as_deref(), current_module, &mut items, )?; @@ -356,7 +356,7 @@ impl Backend { &needle, range.map_unit(ByteOffset), rope, - Some(&[ImStr::from_static("res.groups")]), + Some(&["res.groups".into()]), current_module, &mut items, )?; @@ -513,9 +513,10 @@ impl Backend { } else if current.kind() == "string" && parent.kind() == "subscript" && let Some(lhs) = parent.named_child(0) - && let Some((Type::DictBag(dict), _)) = + && let Some((tid, _)) = self.index .type_of_range(root, dbg!(lhs).byte_range().map_unit(ByteOffset), &contents) + && let Type::DictBag(dict) = type_cache().resolve(tid) { let mut items = MaxVec::new(completions_limit); let dict = dict.into_iter().flat_map(|(key, _)| match key { diff --git a/src/python/diagnostics.rs b/src/python/diagnostics.rs index 72218c7..c4535f6 100644 --- a/src/python/diagnostics.rs +++ b/src/python/diagnostics.rs @@ -4,6 +4,7 @@ use tower_lsp_server::lsp_types::{Diagnostic, DiagnosticRelatedInformation, Diag use tracing::{debug, warn}; use tree_sitter::{Node, QueryCursor, QueryMatch}; +use crate::analyze::type_cache; use crate::index::{_R, Index}; use crate::prelude::*; @@ -306,11 +307,11 @@ impl Backend { }; let mut scope = Scope::default(); let self_type = match self_type { - Some(type_) => ImStr::from(&contents[type_.byte_range().shrink(1)]), - None => ImStr::from_static(""), + Some(type_) => &contents[type_.byte_range().shrink(1)], + None => "", }; scope.super_ = Some(self_param.into()); - scope.insert(self_param.to_string(), Type::Model(self_type)); + scope.insert(self_param.to_string(), Type::Model(self_type.into())); let scope_end = fn_scope.end_byte(); Index::walk_scope(fn_scope, Some(scope), |scope, node| { let entered = (self.index).build_scope(scope, node, scope_end, contents)?; @@ -336,6 +337,7 @@ impl Backend { let Some(lhs_t) = (self.index).type_of(node.child_by_field_name("object").unwrap(), scope, contents) else { return ControlFlow::Continue(entered); }; + let lhs_t = type_cache().resolve(lhs_t); let Some(model_name) = (self.index).try_resolve_model(&lhs_t, scope) else { return ControlFlow::Continue(entered); diff --git a/src/server.rs b/src/server.rs index ebfd938..80b2aa1 100644 --- a/src/server.rs +++ b/src/server.rs @@ -7,6 +7,7 @@ use serde_json::Value; use tower_lsp_server::LanguageServer; use tower_lsp_server::jsonrpc::Result; use tower_lsp_server::lsp_types::notification::{DidChangeConfiguration, Notification}; +use tower_lsp_server::lsp_types::request::WorkDoneProgressCreate; use tower_lsp_server::{UriExt, lsp_types::*}; use tracing::{debug, error, info, instrument, warn}; @@ -59,6 +60,14 @@ impl LanguageServer for Backend { self.capabilities.pull_diagnostics.store(true, Relaxed); } + if let Some(WindowClientCapabilities { + work_done_progress: Some(true), + .. + }) = params.capabilities.window + { + self.capabilities.can_create_wdp.store(true, Relaxed); + } + Ok(InitializeResult { server_info: Some(ServerInfo { name: NAME.to_string(), @@ -200,6 +209,23 @@ impl LanguageServer for Backend { } }; + let mut progress = None; + let token = ProgressToken::String(format!("odoo_lsp_open:{file_path_str}")); + if self.capabilities.can_create_wdp.load(Relaxed) + && self + .client + .send_request::(WorkDoneProgressCreateParams { token: token.clone() }) + .await + .is_ok() + { + progress = Some( + self.client + .progress(token, format!("Opening {file_path_str}")) + .begin() + .await, + ); + } + let rope = Rope::from_str(¶ms.text_document.text); self.document_map.insert( params.text_document.uri.path().as_str().to_string(), @@ -239,6 +265,10 @@ impl LanguageServer for Backend { }) .await .inspect_err(|err| warn!("{err}")); + + if let Some(progress) = progress { + progress.finish().await; + } } #[instrument(skip_all, ret, fields(uri = params.text_document.uri.as_str()))] async fn did_change(&self, mut params: DidChangeTextDocumentParams) { diff --git a/src/str.rs b/src/str.rs index 17fe915..ac7eca1 100644 --- a/src/str.rs +++ b/src/str.rs @@ -6,7 +6,6 @@ use std::path::Path; use std::sync::Arc; use const_format::assertcp_eq; -use num_enum::IntoPrimitive; use num_enum::TryFromPrimitive; /// Immutable, [`String`]-sized clone-friendly string. @@ -18,12 +17,11 @@ const INLINE_BYTES: usize = if cfg!(target_pointer_width = "64") { 23 } else { 1 pub(crate) enum Repr { Arc(Arc), Inline(u23, [u8; INLINE_BYTES]), - Static(&'static str), } #[allow(non_camel_case_types)] #[rustfmt::skip] -#[derive(IntoPrimitive, TryFromPrimitive, Clone, Copy)] +#[derive(TryFromPrimitive, Clone, Copy)] #[repr(u8)] pub(crate) enum u23 { _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, @@ -37,7 +35,7 @@ impl Deref for ImStr { fn deref(&self) -> &Self::Target { match &self.0 { Repr::Arc(inner) => inner, - Repr::Static(inner) => inner, + // Repr::Static(inner) => inner, Repr::Inline(len, bytes) => { let slice = &bytes[..(*len as usize)]; unsafe { std::str::from_utf8_unchecked(slice) } @@ -144,10 +142,6 @@ impl ImStr { pub fn as_str(&self) -> &str { self.deref() } - #[inline] - pub const fn from_static(inner: &'static str) -> Self { - Self(Repr::Static(inner)) - } } const _: () = { diff --git a/src/xml.rs b/src/xml.rs index 36e3c3d..c86299f 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -13,7 +13,7 @@ use xmlparser::{ElementEnd, Error, StrSpan, StreamError, Token, Tokenizer}; use crate::prelude::*; -use crate::analyze::{Scope, Type, normalize}; +use crate::analyze::{Scope, Type, normalize, type_cache}; use crate::component::{ComponentTemplate, PropType}; use crate::index::Index; use crate::model::{Field, FieldKind, PropertyKind}; @@ -302,6 +302,7 @@ impl Backend { let ast = some!(parser.parse(needle, None)); let (object, field, _) = some!(Self::attribute_node_at_offset(py_offset, ast.root_node(), needle)); let model = some!(self.index.type_of(object, &scope, needle)); + let model = type_cache().resolve(model); let model = some!(self.index.try_resolve_model(&model, &Scope::default())); self.index.jump_def_property_name(field, _R(model)) } @@ -442,6 +443,7 @@ impl Backend { })); }; let model = some!(self.index.type_of(object, &scope, needle)); + let model = type_cache().resolve(model); let model = some!(self.index.try_resolve_model(&model, &scope)); let anchor = ref_range.start + relative_offset; self.index.hover_property_name( @@ -689,6 +691,7 @@ impl Index { }); let scope = scope.unwrap_or(default_scope); let model = some!(self.type_of(object, &scope, value)); + let model = type_cache().resolve(model); let model = some!(self.try_resolve_model(&model, &scope)); let needle_end = py_offset.saturating_sub(range.start); let mut needle = field; @@ -873,11 +876,11 @@ impl Index { ref_at_cursor = Some((inner, start_offset..end_offset)); } ref_kind = Some(RefKind::Id); - model_filter = Some(ACTION_MODELS.iter().map(|&s| ImStr::from_static(s)).collect()); + model_filter = Some(ACTION_MODELS.iter().map(|&s| s.into()).collect()); } else if button_type == Some("action") { ref_at_cursor = Some((value.as_str(), value.range())); ref_kind = Some(RefKind::Id); - model_filter = Some(ACTION_MODELS.iter().map(|&s| ImStr::from_static(s)).collect()); + model_filter = Some(ACTION_MODELS.iter().map(|&s| s.into()).collect()); } else { ref_at_cursor = Some((value.as_str(), value.range())); ref_kind = Some(RefKind::MethodName(vec![])); @@ -935,12 +938,12 @@ impl Index { "action" => { ref_at_cursor = Some((value.as_str(), value.range())); ref_kind = Some(RefKind::Id); - model_filter = Some(ACTION_MODELS.iter().map(|&s| ImStr::from_static(s)).collect()); + model_filter = Some(ACTION_MODELS.iter().map(|&s| s.into()).collect()); } "groups" => { ref_kind = Some(RefKind::Id); arch_model = None; - model_filter = Some(vec![ImStr::from_static("res.groups")]); + model_filter = Some(vec!["res.groups".into()]); determine_csv_xmlid_subgroup_of_xmlspan(&mut ref_at_cursor, value, offset_at_cursor); } _ => {} @@ -1165,7 +1168,10 @@ impl Index { contents: &str, ) -> anyhow::Result<()> { normalize(&mut root); - let type_ = self.type_of(root, scope, contents).unwrap_or(Type::Value); + let type_ = match self.type_of(root, scope, contents) { + Some(tid) => type_cache().resolve(tid), + None => Type::Value, + }; let type_ = self .try_resolve_model(&type_, scope) .map(|model| Type::Model(_R(model).into())) diff --git a/testing/Cargo.toml b/testing/Cargo.toml index 5d42e98..967af03 100644 --- a/testing/Cargo.toml +++ b/testing/Cargo.toml @@ -13,7 +13,7 @@ async-lsp = { version = "0.2.2", features = ["tokio", "forward"] } tokio-util = { version = "0.7.14", features = ["compat"] } tree-sitter-xml = "0.7.0" tree-sitter-javascript = "0.23.1" -rstest = "0.25.0" +rstest = { version = "0.25.0", features = ["async-timeout"] } ts-macros.workspace = true pretty_assertions.workspace = true tower.workspace = true diff --git a/testing/fixtures/python_types/bakery/models/models.py b/testing/fixtures/python_types/bakery/models/models.py index d9144cd..43a68ca 100644 --- a/testing/fixtures/python_types/bakery/models/models.py +++ b/testing/fixtures/python_types/bakery/models/models.py @@ -6,7 +6,7 @@ class Bread(models.Model): def _test(self): items = {item: item for item in self} - #^type Dict([Model("bakery.bread"), Model("bakery.bread")]) + #^type Dict(Model("bakery.bread"), Model("bakery.bread")) foobar = {'a': self, 'b': 123} #^type DictBag([("a", Model("bakery.bread")), ("b", PyBuiltin("int"))]) @@ -67,7 +67,7 @@ def test_sanity(self): foobar = ['what'] #^type List(PyBuiltin("str")) self._fields - #^type Dict([PyBuiltin("str"), Model("ir.model.fields")]) + #^type Dict(PyBuiltin("str"), Model("ir.model.fields")) def test_builtins(self): for aaaa, bbbb in enumerate(self): diff --git a/testing/src/tests.rs b/testing/src/tests.rs index bbe1dd6..dda86b8 100644 --- a/testing/src/tests.rs +++ b/testing/src/tests.rs @@ -30,8 +30,8 @@ fn init_tracing() { } #[rstest] -#[timeout(Duration::from_secs(1))] #[tokio::test(flavor = "multi_thread")] +#[timeout(Duration::from_millis(800))] async fn fixture_test(#[files("fixtures/*")] root: PathBuf) { std::env::set_current_dir(&root).unwrap(); let mut server = server::setup_lsp_server(None); @@ -93,7 +93,7 @@ async fn fixture_test(#[files("fixtures/*")] root: PathBuf) { uri: uri.clone(), language_id, version: 1, - text: text.clone(), + text, }, }); From 7d6105d25a82c323943afafd81d9b40674fcbd28 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Mon, 15 Dec 2025 12:07:00 -0500 Subject: [PATCH 09/15] threadid semaphore --- Cargo.lock | 29 ++++++++++++++++++++ Cargo.toml | 1 + src/utils.rs | 47 +++++++++++++++---------------- src/utils/atomic.rs | 67 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 23 deletions(-) create mode 100644 src/utils/atomic.rs diff --git a/Cargo.lock b/Cargo.lock index 8a12a1d..39bd170 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,6 +199,26 @@ version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "bytes" version = "1.11.0" @@ -1473,6 +1493,7 @@ version = "0.6.1" dependencies = [ "anyhow", "bitflags 2.10.0", + "bytemuck", "const_format", "dashmap 6.1.0", "derive_more", @@ -1750,6 +1771,14 @@ dependencies = [ "unicase", ] +[[package]] +name = "python-stubfinder" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "qp-trie" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 2056270..90cf743 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,6 +103,7 @@ globwalk.workspace = true ts-macros.workspace = true tracing-subscriber.workspace = true mini-moka = "0.10.3" +bytemuck = { version = "1.24.0", features = ["derive"] } [dev-dependencies] pretty_assertions.workspace = true diff --git a/src/utils.rs b/src/utils.rs index 9e62f30..b5784c1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,10 +1,9 @@ +use core::fmt::{Debug, Display}; +use core::future::Future; use core::ops::{Add, Sub}; use std::borrow::Cow; use std::ffi::OsStr; -use std::fmt::Display; -use std::future::Future; use std::path::Path; -use std::sync::atomic::{AtomicBool, Ordering}; use dashmap::try_result::TryResult; use futures::future::BoxFuture; @@ -20,6 +19,9 @@ pub use visitor::PreTravel; mod catch_panic; pub use catch_panic::CatchPanic; +mod atomic; +use atomic::{AtomicCell, PodThreadId}; + use crate::index::PathSymbol; use crate::prelude::*; @@ -535,37 +537,32 @@ pub fn init_for_test() { }); } +#[derive(Default)] pub struct Semaphore { - should_wait: AtomicBool, + blocking_thread: AtomicCell>, notifier: tokio::sync::Notify, } -impl Default for Semaphore { - fn default() -> Self { - Self { - should_wait: AtomicBool::new(true), - notifier: Default::default(), - } - } -} - pub struct Blocker<'a>(&'a Semaphore); impl Semaphore { - pub fn block(&self) -> Blocker<'_> { - self.should_wait.store(true, Ordering::SeqCst); + pub async fn block(&self) -> Blocker<'_> { + self.wait().await; + self.blocking_thread.set(Some(std::thread::current().id().into())); Blocker(self) } - pub const WAIT_LIMIT: std::time::Duration = std::time::Duration::from_secs(10); + pub const WAIT_LIMIT: std::time::Duration = std::time::Duration::from_secs(15); /// Waits for a maximum of [`WAIT_LIMIT`][Self::WAIT_LIMIT] for a notification. pub async fn wait(&self) { - if self.should_wait.load(Ordering::SeqCst) { - tokio::select! { - _ = self.notifier.notified() => {} - _ = tokio::time::sleep(Self::WAIT_LIMIT) => { - warn!("WAIT_LIMIT elapsed (thread={:?})", std::thread::current().id()); + if self.should_wait() { + loop { + tokio::select! { + _ = self.notifier.notified() => return, + _ = tokio::time::sleep(Self::WAIT_LIMIT) => { + warn!("WAIT_LIMIT elapsed (thread={:?})", std::thread::current().id()); + } } } } @@ -573,13 +570,17 @@ impl Semaphore { #[inline] pub fn should_wait(&self) -> bool { - self.should_wait.load(Ordering::SeqCst) + // This is not quite correct, but allows us to prevent some simple deadlocks + // when the same thread tries to acquire a blocker + self.blocking_thread + .get() + .is_some_and(|id| id != std::thread::current().id().into()) } } impl Drop for Blocker<'_> { fn drop(&mut self) { - self.0.should_wait.store(false, Ordering::SeqCst); + self.0.blocking_thread.set(None); self.0.notifier.notify_waiters(); } } diff --git a/src/utils/atomic.rs b/src/utils/atomic.rs new file mode 100644 index 0000000..5e90330 --- /dev/null +++ b/src/utils/atomic.rs @@ -0,0 +1,67 @@ +//! Hazmat module to isolate unsafe operations to and from atomic values. + +use core::marker::PhantomData; +use core::num::NonZero; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::thread::ThreadId; + +use bytemuck::{NoUninit, Pod, PodInOption, ZeroableInOption}; + +#[derive(NoUninit, Clone, Copy, PartialEq, Eq)] +#[repr(transparent)] +pub(super) struct PodThreadId(NonZero); + +// SAFETY: NonZero is also implemented these by bytemuck +unsafe impl ZeroableInOption for PodThreadId {} +unsafe impl PodInOption for PodThreadId {} + +impl From for PodThreadId { + fn from(value: ThreadId) -> Self { + const { assert!(size_of::() == size_of::()) } + // SAFETY: Guaranteed by the above assertion + unsafe { core::mem::transmute(value) } + } +} + +impl From for ThreadId { + fn from(value: PodThreadId) -> Self { + const { assert!(size_of::() == size_of::()) } + // SAFETY: Guaranteed by the above assertion + unsafe { core::mem::transmute(value) } + } +} + +/// In-house replacement for [std::sync::atomic::Atomic] while it is unstable. +#[repr(transparent)] +pub(super) struct AtomicCell(AtomicUsize, PhantomData); + +impl AtomicCell { + fn checked_transmute(inner: T) -> usize { + const { + assert!( + size_of::() <= size_of::(), + "Cannot instantiate AtomicCell where T is bigger than usize" + ); + } + let mut buf = [0u8; size_of::()]; + let src = bytemuck::bytes_of(&inner); + buf[..src.len()].copy_from_slice(src); + usize::from_ne_bytes(buf) + } + pub fn new(inner: T) -> Self { + Self(AtomicUsize::new(Self::checked_transmute(inner)), PhantomData) + } + pub fn set(&self, inner: T) { + self.0.store(Self::checked_transmute(inner), Ordering::SeqCst) + } + pub fn get(&self) -> T { + let value = self.0.load(Ordering::SeqCst).to_ne_bytes(); + *bytemuck::from_bytes(&value[..size_of::()]) + } +} + +impl Default for AtomicCell { + fn default() -> Self { + Self::new(Default::default()) + } +} From a8b1f32f4f6d7a6548831305aa6c63ed89f15293 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Wed, 17 Dec 2025 23:20:27 -0500 Subject: [PATCH 10/15] stubfinder --- crates/python-stubfinder/Cargo.toml | 9 +++ crates/python-stubfinder/src/main.rs | 104 +++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 crates/python-stubfinder/Cargo.toml create mode 100644 crates/python-stubfinder/src/main.rs diff --git a/crates/python-stubfinder/Cargo.toml b/crates/python-stubfinder/Cargo.toml new file mode 100644 index 0000000..85c4ec0 --- /dev/null +++ b/crates/python-stubfinder/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "python-stubfinder" +version = "0.1.0" +edition = "2024" + +[dependencies] +# globwalk.workspace = true +serde.workspace = true +serde_json.workspace = true diff --git a/crates/python-stubfinder/src/main.rs b/crates/python-stubfinder/src/main.rs new file mode 100644 index 0000000..439f6ed --- /dev/null +++ b/crates/python-stubfinder/src/main.rs @@ -0,0 +1,104 @@ +use std::{ + env::args, + fs::File, + io::{Read, Write}, + path::{Path, PathBuf}, + process::{Command, Stdio, exit}, +}; + +use serde::Deserialize; + +fn main() { + let Some(mut module) = args().nth(1) else { + eprintln!("At least one Python module path is required!"); + exit(1); + }; + + let mut venv = std::env::var("VIRTUAL_ENV").ok().map(PathBuf::from); + if venv.is_none() { + let path = std::env::current_dir().unwrap().join(".venv"); + if path.exists() { + venv = Some(path); + } + } + + let Some(venv) = venv else { + eprintln!("No VIRTUAL_ENV defined and no .venv found at the working directory!"); + exit(1); + }; + + let python = venv.join("bin/python"); + let (stream, mut sink) = std::io::pipe().unwrap(); + let child = Command::new(python) + .arg("-") + .stdin(stream) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + _ = sink.write_all( + br#" +import sysconfig, site, json +print(json.dumps({ +'stdlib':sysconfig.get_path('stdlib'), +'site':site.getsitepackages(), +}))"#, + ); + drop(sink); + + #[derive(Deserialize)] + struct Output { + stdlib: String, + site: Vec, + } + + let result = child.wait_with_output().unwrap(); + let result: Output = serde_json::from_slice(&result.stdout).unwrap(); + + let mut alternatives = vec![result.stdlib]; + alternatives.extend(result.site); + module = module.split('.').collect::>().join("/"); + + for alternative in alternatives { + let path = Path::new(&alternative).join(&module).with_added_extension("pyi"); + if path.exists() { + print_and_quit(&module, &path); + } + + let path = Path::new(&alternative).join(&module).join("__init__.pyi"); + if path.exists() { + print_submodules(&path); + print_and_quit(&module, &path); + } + + let path = Path::new(&alternative).join(&module).with_added_extension("py"); + if path.exists() { + print_and_quit(&module, &path); + } + + let path = Path::new(&alternative).join(&module).join("__init__.py"); + if path.exists() { + print_submodules(&path); + print_and_quit(&module, &path); + } + } + + println!("Module {module} could not be resolved"); + exit(1); +} + +fn print_submodules(path: &Path) { + println!("# Visible child modules:"); + let root = path.parent().unwrap(); + for file in root.read_dir().unwrap() { + let Ok(file) = file else { continue }; + if file.path() != path { + println!("# {}", file.path().display()); + } + } +} + +fn print_and_quit(module: &str, path: &Path) -> ! { + println!("# {module} defined in {}:", path.display()); + std::io::copy(&mut File::open(path).unwrap(), &mut std::io::stdout()).unwrap(); + exit(0); +} From b55eb2d9963ac810ccd7c6bc320cca73d2112641 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Thu, 18 Dec 2025 17:20:18 -0500 Subject: [PATCH 11/15] misc --- src/backend.rs | 9 ++++++--- src/utils.rs | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/backend.rs b/src/backend.rs index ade88b9..01dd655 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -288,6 +288,7 @@ impl Backend { } if eager_diagnostics { + let client = self.client.clone(); let diagnostics = { self.document_map .get(params.uri.path().as_str()) @@ -295,9 +296,11 @@ impl Backend { .diagnostics_cache .clone() }; - self.client - .publish_diagnostics(params.uri, diagnostics, Some(params.version)) - .await; + tokio::spawn(async move { + client + .publish_diagnostics(params.uri, diagnostics, Some(params.version)) + .await + }); } Ok(()) diff --git a/src/utils.rs b/src/utils.rs index b5784c1..ab4173a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -552,7 +552,7 @@ impl Semaphore { Blocker(self) } - pub const WAIT_LIMIT: std::time::Duration = std::time::Duration::from_secs(15); + pub const WAIT_LIMIT: std::time::Duration = std::time::Duration::from_secs(2); /// Waits for a maximum of [`WAIT_LIMIT`][Self::WAIT_LIMIT] for a notification. pub async fn wait(&self) { From 81d81efcbbe07a814438df633d2a0f103ac1bbb9 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Fri, 19 Dec 2025 15:46:49 -0500 Subject: [PATCH 12/15] upgrade tower-lsp-server --- Cargo.lock | 60 +++++++++++++++------ Cargo.toml | 2 +- crates/python-stubfinder/src/main.rs | 12 ++--- justfile | 2 +- src/analyze.rs | 2 +- src/backend.rs | 8 +-- src/component.rs | 2 +- src/index.rs | 36 ++++++++++--- src/js.rs | 2 +- src/main.rs | 20 +++---- src/prelude.rs | 5 +- src/python.rs | 4 +- src/python/completions.rs | 3 +- src/python/diagnostics.rs | 2 +- src/record.rs | 2 +- src/server.rs | 78 +++++++++------------------- src/template.rs | 2 +- src/utils.rs | 26 ++++++++-- src/utils/catch_panic.rs | 2 +- src/xml.rs | 2 +- src/xml/tests.rs | 1 - testing/src/tests.rs | 2 +- 22 files changed, 154 insertions(+), 121 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 39bd170..e0bfd30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,7 +69,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c1c85c4bb41706ad1f8338e39fa725a24bc642be41140a38d818c93b9ae91f5" dependencies = [ "futures", - "lsp-types 0.95.1", + "lsp-types", "pin-project-lite", "rustix", "serde", @@ -177,6 +177,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + [[package]] name = "bstr" version = "1.12.1" @@ -641,11 +647,12 @@ dependencies = [ [[package]] name = "fluent-uri" -version = "0.1.4" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" dependencies = [ - "bitflags 1.3.2", + "borrow-or-share", + "ref-cast", ] [[package]] @@ -1337,29 +1344,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] -name = "lsp-types" -version = "0.95.1" +name = "ls-types" +version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e34d33a8e9b006cd3fc4fe69a921affa097bae4bb65f76271f4644f9a334365" +checksum = "7a7deb98ef9daaa7500324351a5bab7c80c644cfb86b4be0c4433b582af93510" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.10.0", + "fluent-uri", + "percent-encoding", "serde", "serde_json", - "serde_repr", - "url", ] [[package]] name = "lsp-types" -version = "0.97.0" +version = "0.95.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071" +checksum = "8e34d33a8e9b006cd3fc4fe69a921affa097bae4bb65f76271f4644f9a334365" dependencies = [ "bitflags 1.3.2", - "fluent-uri", "serde", "serde_json", "serde_repr", + "url", ] [[package]] @@ -1935,6 +1942,26 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex" version = "1.12.2" @@ -2693,17 +2720,16 @@ checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-lsp-server" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88f3f8ec0dcfdda4d908bad2882fe0f89cf2b606e78d16491323e918dfa95765" +checksum = "2f0e711655c89181a6bc6a2cc348131fcd9680085f5b06b6af13427a393a6e72" dependencies = [ "bytes", "dashmap 6.1.0", "futures", "httparse", - "lsp-types 0.97.0", + "ls-types", "memchr", - "percent-encoding", "serde", "serde_json", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 90cf743..218b82c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ tree-sitter = "0.25" ts-macros = { version = "0.5.0", path = "crates/ts-macros" } pretty_assertions = "1.4.0" tokio = { version = "1.46.0", default-features = false, features = ["macros", "rt-multi-thread", "fs", "io-std", "io-util", "net"] } -tower-lsp-server = { version = "0.22.0", features = ["proposed"] } +tower-lsp-server = { version = "0.23.0", features = ["proposed"] } tower = { version = "0.5", features = ["timeout"] } futures = "0.3.31" tree-sitter-python = "0.23.6" diff --git a/crates/python-stubfinder/src/main.rs b/crates/python-stubfinder/src/main.rs index 439f6ed..ffd9820 100644 --- a/crates/python-stubfinder/src/main.rs +++ b/crates/python-stubfinder/src/main.rs @@ -1,10 +1,8 @@ -use std::{ - env::args, - fs::File, - io::{Read, Write}, - path::{Path, PathBuf}, - process::{Command, Stdio, exit}, -}; +use std::env::args; +use std::fs::File; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio, exit}; use serde::Deserialize; diff --git a/justfile b/justfile index 24872cb..198021c 100644 --- a/justfile +++ b/justfile @@ -1,4 +1,4 @@ -test *args: (ensure_cargo "cargo-nextest") +test *args="--no-fail-fast": (ensure_cargo "cargo-nextest") cargo nextest run -p odoo-lsp -p odoo-lsp-tests {{args}} bench: (ensure_cargo "iai-callgrind-runner") diff --git a/src/analyze.rs b/src/analyze.rs index bcbaf46..0898c28 100644 --- a/src/analyze.rs +++ b/src/analyze.rs @@ -1487,7 +1487,7 @@ pub fn determine_scope<'out, 'node>( mod tests { use pretty_assertions::assert_eq; use ropey::Rope; - use tower_lsp_server::lsp_types::Position; + use tower_lsp_server::ls_types::Position; use tree_sitter::{Parser, QueryCursor, StreamingIterator, StreamingIteratorMut}; use crate::analyze::{FieldCompletion, Type, type_cache}; diff --git a/src/backend.rs b/src/backend.rs index 01dd655..065998f 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -16,7 +16,7 @@ use fomat_macros::fomat; use globwalk::FileType; use smart_default::SmartDefault; use tower_lsp_server::Client; -use tower_lsp_server::{UriExt, lsp_types::*}; +use tower_lsp_server::ls_types::*; use tree_sitter::{Parser, Tree}; use crate::analyze::{Scope, Type, TypeId, type_cache}; @@ -224,7 +224,7 @@ impl Backend { } } - #[instrument(skip_all)] + #[instrument(skip_all, ret)] pub async fn on_change(&self, params: TextDocumentItem) -> anyhow::Result<()> { let split_uri = params.uri.path().as_str().rsplit_once('.'); let rope; @@ -838,7 +838,7 @@ impl Index { (origin_fragment) } } - #[instrument(skip_all, fields(name, type_))] + #[instrument(level = "trace", skip_all, fields(name, type_))] pub fn hover_variable( &self, name: Option<&str>, @@ -914,7 +914,7 @@ impl Index { if let Some(help) = &field.help { (help.to_string()) } } } - #[instrument(skip_all, fields(name, model))] + #[instrument(level = "trace", skip_all, fields(name, model))] pub fn hover_property_name(&self, name: &str, model: &str, range: Option) -> anyhow::Result> { let model_key = _I(model); let entry = some!(self.models.populate_properties(model_key.into(), &[])); diff --git a/src/component.rs b/src/component.rs index cadeb50..0484a4d 100644 --- a/src/component.rs +++ b/src/component.rs @@ -2,7 +2,7 @@ //! //! [Owl]: https://odoo.github.io/owl/ -use tower_lsp_server::lsp_types::Range; +use tower_lsp_server::ls_types::Range; use crate::index::{Symbol, SymbolMap, TemplateIndex}; use crate::template::TemplateName; diff --git a/src/index.rs b/src/index.rs index 65c177c..7e7a712 100644 --- a/src/index.rs +++ b/src/index.rs @@ -14,7 +14,7 @@ use ignore::Match; use ignore::gitignore::Gitignore; use lasso::ThreadedRodeo; use smart_default::SmartDefault; -use tower_lsp_server::{Client, lsp_types::*}; +use tower_lsp_server::{Client, ls_types::*}; use xmlparser::{Token, Tokenizer}; use crate::prelude::*; @@ -214,7 +214,8 @@ impl Index { // Clear transitive dependencies cache since modules have been added/modified self.transitive_deps_cache.clear(); - // After adding all modules in the root, check for auto_install modules with unsatisfied dependencies // We pass an empty set because no modules are loaded yet, this will identify all auto_install + // After adding all modules in the root, check for auto_install modules with unsatisfied dependencies + // We pass an empty set because no modules are loaded yet, this will identify all auto_install // modules and track those with missing dependencies debug!("Checking for auto_install modules after adding root"); let auto_install_check = self.find_auto_install_modules(&HashSet::new()); @@ -222,6 +223,29 @@ impl Index { Ok(()) } + #[instrument(skip(self), ret)] + pub async fn add_root_for_file(&self, path: &Path) { + if self.find_module_of(path).is_some() { + return; + } + + debug!("oob: {}", path.display()); + let mut path = Some(path); + while let Some(path_) = path { + if tokio::fs::try_exists(path_.with_file_name("__manifest__.py")) + .await + .unwrap_or(false) + && let Some(file_path) = path_.parent().and_then(|p| p.parent()) + { + _ = self + .add_root(file_path, None) + .await + .inspect_err(|err| warn!("failed to add root {}:\n{err}", file_path.display())); + return; + } + path = path_.parent(); + } + } #[instrument(skip_all, ret, fields(path = path.display().to_string()))] pub(crate) async fn load_modules_for_document(&self, document_blocker: Arc, path: &Path) -> Option<()> { let _blocker = document_blocker.block(); @@ -546,17 +570,17 @@ impl Index { let mut outputs = tokio::task::JoinSet::new(); for (module_key, root) in modules.into_iter().zip(roots.into_iter()) { - info!("{} depends on {}", _R(module_name), _R(module_key)); let root_display = root.key().to_string_lossy(); let root_key = _I(&root_display); let module = root .get(&module_key) .expect(format_loc!("module must already be present by now")); - let module_dir = Path::new(&*root_display).join(&module.path); if module.loaded.compare_exchange(false, true, Relaxed, Relaxed) != Ok(false) { continue; } + info!("{} depends on {}", _R(module_name), _R(module_key)); + let module_dir = Path::new(&*root_display).join(&module.path); if let Ok(xmls) = globwalk::glob_builder(format!("{}/**/*.xml", module_dir.display())) .file_type(FileType::FILE | FileType::SYMLINK) .follow_links(true) @@ -646,7 +670,7 @@ impl Index { } } } - debug!( + trace!( "Currently loaded modules: {:?}", all_loaded_modules.iter().map(|m| _R(*m)).collect::>() ); @@ -659,7 +683,7 @@ impl Index { auto_install_candidates.len() ); for auto_module in auto_install_candidates { - info!( + trace!( "Auto-installing module {} because all dependencies are satisfied", _R(auto_module) ); diff --git a/src/js.rs b/src/js.rs index a0e3b70..d7e7301 100644 --- a/src/js.rs +++ b/src/js.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use std::sync::atomic::Ordering::Relaxed; -use tower_lsp_server::lsp_types::*; +use tower_lsp_server::ls_types::*; use tree_sitter::{QueryCursor, Tree}; use crate::prelude::*; diff --git a/src/main.rs b/src/main.rs index f7dc9d0..5c187c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -104,16 +104,13 @@ fn main() { } _ => {} } + let threads = threads.max(1); - let rt = if threads <= 1 { - tokio::runtime::Builder::new_current_thread().enable_all().build() - } else { - tokio::runtime::Builder::new_multi_thread() - .worker_threads(threads) - .enable_all() - .build() - } - .expect("failed to build runtime"); + let rt = tokio::runtime::Builder::new_multi_thread() + .worker_threads(threads) + .enable_all() + .build() + .expect("failed to build runtime"); rt.block_on(async move { if cli::run(args).await { return; @@ -139,7 +136,10 @@ fn main() { .layer(tower::timeout::TimeoutLayer::new(Duration::from_secs(30))) .layer_fn(CatchPanic) .service(service); - Server::new(stdin, stdout, socket).serve(service).await; + Server::new(stdin, stdout, socket) + .concurrency_level(2) + .serve(service) + .await; }) } diff --git a/src/prelude.rs b/src/prelude.rs index 00dceed..e76fc68 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -6,9 +6,8 @@ pub use lasso::{Key, Spur}; pub use ropey::LineType::LF_CR as LINE_TYPE; pub use ropey::{Rope, RopeSlice}; pub use serde::{Deserialize, Serialize}; -pub use tower_lsp_server::UriExt; -pub use tower_lsp_server::lsp_types::Range; -pub use tower_lsp_server::lsp_types::Uri; +pub use tower_lsp_server::ls_types::Range; +pub use tower_lsp_server::ls_types::Uri; pub use tracing::{debug, error, info, instrument, trace, warn}; pub use tree_sitter::{Node, Parser, QueryCursor, StreamingIterator, StreamingIteratorMut}; pub use ts_macros::query; diff --git a/src/python.rs b/src/python.rs index e8033af..626c3fe 100644 --- a/src/python.rs +++ b/src/python.rs @@ -3,7 +3,7 @@ use std::path::Path; use lasso::Spur; use ropey::Rope; -use tower_lsp_server::{UriExt, lsp_types::*}; +use tower_lsp_server::ls_types::*; use tracing::{debug, instrument, trace, warn}; use tree_sitter::{Node, Parser, QueryMatch}; use ts_macros::query; @@ -299,7 +299,7 @@ impl Backend { })) } - #[tracing::instrument(skip_all, fields(uri))] + #[tracing::instrument(skip_all, ret, fields(uri))] pub fn on_change_python( &self, text: &Text, diff --git a/src/python/completions.rs b/src/python/completions.rs index c88b388..33f83d5 100644 --- a/src/python/completions.rs +++ b/src/python/completions.rs @@ -1,10 +1,9 @@ -use tower_lsp_server::lsp_types::{CompletionList, CompletionResponse}; use tree_sitter::{Node, QueryMatch}; use std::borrow::Cow; use std::sync::atomic::Ordering::Relaxed; -use tower_lsp_server::{UriExt, lsp_types::*}; +use tower_lsp_server::ls_types::*; use tracing::debug; use tree_sitter::Tree; diff --git a/src/python/diagnostics.rs b/src/python/diagnostics.rs index c4535f6..4181f61 100644 --- a/src/python/diagnostics.rs +++ b/src/python/diagnostics.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, cmp::Ordering, ops::ControlFlow}; -use tower_lsp_server::lsp_types::{Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, Location}; +use tower_lsp_server::ls_types::{Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, Location}; use tracing::{debug, warn}; use tree_sitter::{Node, QueryCursor, QueryMatch}; diff --git a/src/record.rs b/src/record.rs index 8ff670a..2aa83b3 100644 --- a/src/record.rs +++ b/src/record.rs @@ -1,6 +1,6 @@ //! XML [records][Record], as well as [`template`][Record::template] and [`menuitem`][Record::menuitem] shorthands. -use tower_lsp_server::lsp_types::*; +use tower_lsp_server::ls_types::*; use xmlparser::{ElementEnd, Token, Tokenizer}; use crate::prelude::*; diff --git a/src/server.rs b/src/server.rs index 80b2aa1..cbc1386 100644 --- a/src/server.rs +++ b/src/server.rs @@ -6,12 +6,12 @@ use ropey::Rope; use serde_json::Value; use tower_lsp_server::LanguageServer; use tower_lsp_server::jsonrpc::Result; -use tower_lsp_server::lsp_types::notification::{DidChangeConfiguration, Notification}; -use tower_lsp_server::lsp_types::request::WorkDoneProgressCreate; -use tower_lsp_server::{UriExt, lsp_types::*}; +use tower_lsp_server::ls_types::notification::{DidChangeConfiguration, Notification}; +use tower_lsp_server::ls_types::request::WorkDoneProgressCreate; +use tower_lsp_server::ls_types::*; use tracing::{debug, error, info, instrument, warn}; -use crate::{GITVER, NAME, VERSION, await_did_open_document}; +use crate::{GITVER, NAME, VERSION, await_did_open_document, format_loc}; use crate::backend::{Backend, Document, Language, Text}; use crate::index::{_G, _R}; @@ -232,27 +232,7 @@ impl LanguageServer for Backend { Document::new(rope.clone()), ); - if self.index.find_module_of(&file_path).is_none() { - // outside of root? - debug!("oob: {}", file_path_str); - let path = params.text_document.uri.to_file_path(); - let mut path = path.as_deref(); - while let Some(path_) = path { - if tokio::fs::try_exists(path_.with_file_name("__manifest__.py")) - .await - .unwrap_or(false) - && let Some(file_path) = path_.parent().and_then(|p| p.parent()) - { - _ = self - .index - .add_root(file_path, Some(self.client.clone())) - .await - .inspect_err(|err| warn!("failed to add root {}:\n{err}", file_path.display())); - break; - } - path = path_.parent(); - } - } + self.index.add_root_for_file(&file_path).await; _ = self .on_change(backend::TextDocumentItem { @@ -267,7 +247,7 @@ impl LanguageServer for Backend { .inspect_err(|err| warn!("{err}")); if let Some(progress) = progress { - progress.finish().await; + tokio::spawn(progress.finish()); } } #[instrument(skip_all, ret, fields(uri = params.text_document.uri.as_str()))] @@ -347,7 +327,7 @@ impl LanguageServer for Backend { }; await_did_open_document!(self, path); - let Some(document) = self.document_map.get(path) else { + let Some(document) = self.document_map.try_get(path).expect(format_loc!("deadlock")) else { panic!("Bug: did not build a document for {}", uri.path().as_str()); }; if document.setup.should_wait() { @@ -517,7 +497,7 @@ impl LanguageServer for Backend { } } } - #[instrument(skip_all, ret, fields(uri = params.text_document_position_params.text_document.uri.as_str()))] + #[instrument(level = "trace", skip_all, ret, fields(uri = params.text_document_position_params.text_document.uri.as_str()))] async fn hover(&self, params: HoverParams) -> Result> { self.root_setup.wait().await; @@ -564,7 +544,7 @@ impl LanguageServer for Backend { for (config, ws) in configs.into_iter().zip(workspace_paths) { match serde_json::from_value(config) { Ok(config) => self.on_change_config(config, Some(&ws)), - Err(err) => error!("Could not parse updated configuration for {}:\n{err}", ws.display()), + Err(err) => warn!("Ignoring config update for {}:\n {err}", ws.display()), } } self.ensure_nonoverlapping_roots(); @@ -589,32 +569,20 @@ impl LanguageServer for Backend { for added in workspaces.difference(&roots) { if let Err(err) = self.index.add_root(added, None).await { - error!("failed to add root {}:\n{err}", added.display()); + error!("failed to add root {}:\n {err}", added.display()); } } } #[instrument(skip(self))] async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) { - self.root_setup.wait().await; for added in params.event.added { - let Some(file_path) = added.uri.to_file_path() else { - error!("not a file path: {}", added.uri.as_str()); - continue; - }; - _ = self - .index - .add_root(&file_path, None) - .await - .inspect_err(|err| warn!("failed to add root {}:\n{err}", file_path.display())); + let Some(path) = added.uri.to_file_path() else { continue }; + self.index.add_root_for_file(&path).await; } for removed in params.event.removed { - let Some(file_path) = removed.uri.to_file_path() else { - error!("not a file path: {}", removed.uri.as_str()); - continue; - }; - self.index.remove_root(&file_path); + warn!("unimplemented removing workspace folder {}", removed.name); } - self.index.delete_marked_entries(); + // self.index.delete_marked_entries(); } /// For VSCode and capable LSP clients, these events represent changes mostly to configuration files. #[instrument(skip(self))] @@ -655,16 +623,16 @@ impl LanguageServer for Backend { }); } } - self.client.publish_diagnostics(uri, diagnostics, None).await; + if !diagnostics.is_empty() { + let client = self.client.clone(); + tokio::spawn(async move { client.publish_diagnostics(uri, diagnostics, None).await }); + } break; } } } #[instrument(skip_all, fields(query = params.query))] - async fn symbol( - &self, - params: WorkspaceSymbolParams, - ) -> Result, Vec>>> { + async fn symbol(&self, params: WorkspaceSymbolParams) -> Result> { self.root_setup.wait().await; let query = ¶ms.query; @@ -708,13 +676,17 @@ impl LanguageServer for Backend { .and_then(|record| (record.module == module).then(|| to_symbol_information(&record))) }) }); - Ok(Some(OneOf::Left(models.chain(records).take(limit).collect()))) + Ok(Some(WorkspaceSymbolResponse::Flat( + models.chain(records).take(limit).collect(), + ))) } else { let records = records_by_prefix.iter_prefix(query.as_bytes()).flat_map(|(_, keys)| { keys.iter() .flat_map(|key| self.index.records.get(key).map(|record| to_symbol_information(&record))) }); - Ok(Some(OneOf::Left(models.chain(records).take(limit).collect()))) + Ok(Some(WorkspaceSymbolResponse::Flat( + models.chain(records).take(limit).collect(), + ))) } } #[instrument(skip_all, fields(path))] diff --git a/src/template.rs b/src/template.rs index 932ed5e..8fefc39 100644 --- a/src/template.rs +++ b/src/template.rs @@ -1,5 +1,5 @@ use ropey::RopeSlice; -use tower_lsp_server::lsp_types::{Position, Range}; +use tower_lsp_server::ls_types::{Position, Range}; use xmlparser::{ElementEnd, Token, Tokenizer}; use crate::prelude::*; diff --git a/src/utils.rs b/src/utils.rs index ab4173a..abf1bb8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -9,7 +9,7 @@ use dashmap::try_result::TryResult; use futures::future::BoxFuture; use ropey::RopeSlice; use smart_default::SmartDefault; -use tower_lsp_server::lsp_types::*; +use tower_lsp_server::ls_types::*; use tracing::warn; use xmlparser::{StrSpan, TextPos, Token}; @@ -82,14 +82,19 @@ macro_rules! await_did_open_document { ($self:expr, $path:expr) => { let mut blocker = None; { - if let Some(document) = $self.document_map.get($path) + if let Some(document) = $self + .document_map + .try_get($path) + .expect($crate::format_loc!("deadlock")) && document.setup.should_wait() { - blocker = Some(document.setup.clone()); + blocker = Some(std::sync::Arc::clone(&document.setup)); } } if let Some(blocker) = blocker { + info!("[{}] waiting on {}", $crate::loc!(), $path); blocker.wait().await; + info!("[{}] done waiting on {}", $crate::loc!(), $path); } }; } @@ -546,8 +551,13 @@ pub struct Semaphore { pub struct Blocker<'a>(&'a Semaphore); impl Semaphore { - pub async fn block(&self) -> Blocker<'_> { - self.wait().await; + /// Panics if lock is held by another thread. + #[must_use] + #[track_caller] + pub fn block(&self) -> Blocker<'_> { + if self.should_wait() { + panic!("Misuse of Semaphore::block: lock is still being held!") + } self.blocking_thread.set(Some(std::thread::current().id().into())); Blocker(self) } @@ -580,6 +590,12 @@ impl Semaphore { impl Drop for Blocker<'_> { fn drop(&mut self) { + info!( + "{:?} (task {:?}) releasing blocker on {:p}", + std::thread::current().id(), + tokio::task::try_id(), + self.0 + ); self.0.blocking_thread.set(None); self.0.notifier.notify_waiters(); } diff --git a/src/utils/catch_panic.rs b/src/utils/catch_panic.rs index 84709d7..7fcf702 100644 --- a/src/utils/catch_panic.rs +++ b/src/utils/catch_panic.rs @@ -75,7 +75,7 @@ fn handle_panic_err(err: &dyn Any) { } enum ServerErrors { - PreawaitPanic, + PreawaitPanic = 0x1000, PostawaitPanic, } diff --git a/src/xml.rs b/src/xml.rs index c86299f..4271608 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -6,7 +6,7 @@ use std::sync::atomic::Ordering::Relaxed; use fomat_macros::fomat; use lasso::Spur; -use tower_lsp_server::{UriExt, lsp_types::*}; +use tower_lsp_server::ls_types::*; use tracing::{debug, instrument, warn}; use tree_sitter::Parser; use xmlparser::{ElementEnd, Error, StrSpan, StreamError, Token, Tokenizer}; diff --git a/src/xml/tests.rs b/src/xml/tests.rs index e980730..cafed4b 100644 --- a/src/xml/tests.rs +++ b/src/xml/tests.rs @@ -1,5 +1,4 @@ use super::*; -use crate::xml::CompletionResponse; #[test] fn test_determine_csv_xmlid_subgroup_single() { diff --git a/testing/src/tests.rs b/testing/src/tests.rs index dda86b8..b1edec4 100644 --- a/testing/src/tests.rs +++ b/testing/src/tests.rs @@ -34,7 +34,7 @@ fn init_tracing() { #[timeout(Duration::from_millis(800))] async fn fixture_test(#[files("fixtures/*")] root: PathBuf) { std::env::set_current_dir(&root).unwrap(); - let mut server = server::setup_lsp_server(None); + let mut server = server::setup_lsp_server(Some(2)); init_tracing(); _ = server From 28ffd0d1d165b3c0444bae70b8ea83dba8549a89 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Fri, 19 Dec 2025 16:07:18 -0500 Subject: [PATCH 13/15] undo threadid changes --- Cargo.lock | 21 -------------- Cargo.toml | 1 - src/utils.rs | 42 +++++++++++---------------- src/utils/atomic.rs | 67 -------------------------------------------- testing/src/tests.rs | 2 +- 5 files changed, 17 insertions(+), 116 deletions(-) delete mode 100644 src/utils/atomic.rs diff --git a/Cargo.lock b/Cargo.lock index e0bfd30..92f7fa0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,26 +205,6 @@ version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" -[[package]] -name = "bytemuck" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "bytes" version = "1.11.0" @@ -1500,7 +1480,6 @@ version = "0.6.1" dependencies = [ "anyhow", "bitflags 2.10.0", - "bytemuck", "const_format", "dashmap 6.1.0", "derive_more", diff --git a/Cargo.toml b/Cargo.toml index 218b82c..7ec6118 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,7 +103,6 @@ globwalk.workspace = true ts-macros.workspace = true tracing-subscriber.workspace = true mini-moka = "0.10.3" -bytemuck = { version = "1.24.0", features = ["derive"] } [dev-dependencies] pretty_assertions.workspace = true diff --git a/src/utils.rs b/src/utils.rs index abf1bb8..57c49b6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -4,6 +4,8 @@ use core::ops::{Add, Sub}; use std::borrow::Cow; use std::ffi::OsStr; use std::path::Path; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering::Relaxed; use dashmap::try_result::TryResult; use futures::future::BoxFuture; @@ -19,9 +21,6 @@ pub use visitor::PreTravel; mod catch_panic; pub use catch_panic::CatchPanic; -mod atomic; -use atomic::{AtomicCell, PodThreadId}; - use crate::index::PathSymbol; use crate::prelude::*; @@ -544,7 +543,7 @@ pub fn init_for_test() { #[derive(Default)] pub struct Semaphore { - blocking_thread: AtomicCell>, + blocked: AtomicBool, notifier: tokio::sync::Notify, } @@ -555,10 +554,10 @@ impl Semaphore { #[must_use] #[track_caller] pub fn block(&self) -> Blocker<'_> { - if self.should_wait() { - panic!("Misuse of Semaphore::block: lock is still being held!") + if self.blocked.compare_exchange(false, true, Relaxed, Relaxed) != Ok(false) { + panic!("Misuse of Semaphore::block: lock is still being held!"); } - self.blocking_thread.set(Some(std::thread::current().id().into())); + info!("{:?} blocking {:p}", std::thread::current().id(), self); Blocker(self) } @@ -566,13 +565,11 @@ impl Semaphore { /// Waits for a maximum of [`WAIT_LIMIT`][Self::WAIT_LIMIT] for a notification. pub async fn wait(&self) { - if self.should_wait() { - loop { - tokio::select! { - _ = self.notifier.notified() => return, - _ = tokio::time::sleep(Self::WAIT_LIMIT) => { - warn!("WAIT_LIMIT elapsed (thread={:?})", std::thread::current().id()); - } + while self.should_wait() { + tokio::select! { + _ = self.notifier.notified() => return, + _ = tokio::time::sleep(Self::WAIT_LIMIT) => { + warn!("WAIT_LIMIT elapsed (thread={:?} blocker={:p})", std::thread::current().id(), self); } } } @@ -580,23 +577,16 @@ impl Semaphore { #[inline] pub fn should_wait(&self) -> bool { - // This is not quite correct, but allows us to prevent some simple deadlocks - // when the same thread tries to acquire a blocker - self.blocking_thread - .get() - .is_some_and(|id| id != std::thread::current().id().into()) + self.blocked.load(Relaxed) } } impl Drop for Blocker<'_> { fn drop(&mut self) { - info!( - "{:?} (task {:?}) releasing blocker on {:p}", - std::thread::current().id(), - tokio::task::try_id(), - self.0 - ); - self.0.blocking_thread.set(None); + if self.0.blocked.compare_exchange(true, false, Relaxed, Relaxed) != Ok(true) { + panic!("Failed to release Semaphore lock for {:?}", std::thread::current().id()); + } + info!("{:?} dropping {:p}", std::thread::current().id(), self.0); self.0.notifier.notify_waiters(); } } diff --git a/src/utils/atomic.rs b/src/utils/atomic.rs deleted file mode 100644 index 5e90330..0000000 --- a/src/utils/atomic.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! Hazmat module to isolate unsafe operations to and from atomic values. - -use core::marker::PhantomData; -use core::num::NonZero; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::thread::ThreadId; - -use bytemuck::{NoUninit, Pod, PodInOption, ZeroableInOption}; - -#[derive(NoUninit, Clone, Copy, PartialEq, Eq)] -#[repr(transparent)] -pub(super) struct PodThreadId(NonZero); - -// SAFETY: NonZero is also implemented these by bytemuck -unsafe impl ZeroableInOption for PodThreadId {} -unsafe impl PodInOption for PodThreadId {} - -impl From for PodThreadId { - fn from(value: ThreadId) -> Self { - const { assert!(size_of::() == size_of::()) } - // SAFETY: Guaranteed by the above assertion - unsafe { core::mem::transmute(value) } - } -} - -impl From for ThreadId { - fn from(value: PodThreadId) -> Self { - const { assert!(size_of::() == size_of::()) } - // SAFETY: Guaranteed by the above assertion - unsafe { core::mem::transmute(value) } - } -} - -/// In-house replacement for [std::sync::atomic::Atomic] while it is unstable. -#[repr(transparent)] -pub(super) struct AtomicCell(AtomicUsize, PhantomData); - -impl AtomicCell { - fn checked_transmute(inner: T) -> usize { - const { - assert!( - size_of::() <= size_of::(), - "Cannot instantiate AtomicCell where T is bigger than usize" - ); - } - let mut buf = [0u8; size_of::()]; - let src = bytemuck::bytes_of(&inner); - buf[..src.len()].copy_from_slice(src); - usize::from_ne_bytes(buf) - } - pub fn new(inner: T) -> Self { - Self(AtomicUsize::new(Self::checked_transmute(inner)), PhantomData) - } - pub fn set(&self, inner: T) { - self.0.store(Self::checked_transmute(inner), Ordering::SeqCst) - } - pub fn get(&self) -> T { - let value = self.0.load(Ordering::SeqCst).to_ne_bytes(); - *bytemuck::from_bytes(&value[..size_of::()]) - } -} - -impl Default for AtomicCell { - fn default() -> Self { - Self::new(Default::default()) - } -} diff --git a/testing/src/tests.rs b/testing/src/tests.rs index b1edec4..fe78f0b 100644 --- a/testing/src/tests.rs +++ b/testing/src/tests.rs @@ -31,7 +31,7 @@ fn init_tracing() { #[rstest] #[tokio::test(flavor = "multi_thread")] -#[timeout(Duration::from_millis(800))] +#[timeout(Duration::from_secs(10))] async fn fixture_test(#[files("fixtures/*")] root: PathBuf) { std::env::set_current_dir(&root).unwrap(); let mut server = server::setup_lsp_server(Some(2)); From 13d2838861b256d764809fc3c0a1a6172b544e64 Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Fri, 19 Dec 2025 16:29:13 -0500 Subject: [PATCH 14/15] conditional expression --- src/analyze.rs | 10 ++++++++++ testing/fixtures/python_expressions/.odoo_lsp | 1 + .../fixtures/python_expressions/main/__manifest__.py | 1 + testing/fixtures/python_expressions/main/models.py | 8 ++++++++ 4 files changed, 20 insertions(+) create mode 100644 testing/fixtures/python_expressions/.odoo_lsp create mode 100644 testing/fixtures/python_expressions/main/__manifest__.py create mode 100644 testing/fixtures/python_expressions/main/models.py diff --git a/src/analyze.rs b/src/analyze.rs index 0898c28..10075fb 100644 --- a/src/analyze.rs +++ b/src/analyze.rs @@ -683,6 +683,16 @@ impl Index { self.type_of(node.child_by_field_name("left")?, scope, contents) } + "conditional_expression" => { + // a if b else c + let ty = node + .named_child(0) + .and_then(|child| self.type_of(child, scope, contents)); + ty.or_else(|| { + node.named_child(2) + .and_then(|child| self.type_of(child, scope, contents)) + }) + } "dictionary_comprehension" => { let pair = dig!(node, pair)?; let mut comprehension_scope; diff --git a/testing/fixtures/python_expressions/.odoo_lsp b/testing/fixtures/python_expressions/.odoo_lsp new file mode 100644 index 0000000..6c17534 --- /dev/null +++ b/testing/fixtures/python_expressions/.odoo_lsp @@ -0,0 +1 @@ +{"module":{"roots":["."]}} \ No newline at end of file diff --git a/testing/fixtures/python_expressions/main/__manifest__.py b/testing/fixtures/python_expressions/main/__manifest__.py new file mode 100644 index 0000000..b02b1fb --- /dev/null +++ b/testing/fixtures/python_expressions/main/__manifest__.py @@ -0,0 +1 @@ +{"name": "main"} \ No newline at end of file diff --git a/testing/fixtures/python_expressions/main/models.py b/testing/fixtures/python_expressions/main/models.py new file mode 100644 index 0000000..09ba6ae --- /dev/null +++ b/testing/fixtures/python_expressions/main/models.py @@ -0,0 +1,8 @@ +class Main(models.Model): + _name = 'main' + + def test_expression(self): + foo = self if True else self + #^type Model("main") + bar = self if True else 123 # we just pick an arbitrary one + #^type Model("main") From 6664c456beec56910aabc4a311c9b5f83a34f00e Mon Sep 17 00:00:00 2001 From: Viet Dinh <54ckb0y789@gmail.com> Date: Fri, 19 Dec 2025 16:37:13 -0500 Subject: [PATCH 15/15] fix mapped of non-model --- src/analyze.rs | 5 +++-- testing/fixtures/python_types/bakery/models/models.py | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/analyze.rs b/src/analyze.rs index 10075fb..da73fbe 100644 --- a/src/analyze.rs +++ b/src/analyze.rs @@ -849,8 +849,9 @@ impl Index { let mut model: Spur = model.into(); let mut mapped = &contents[mapped.byte_range().shrink(1)]; self.models.resolve_mapped(&mut model, &mut mapped, None).ok()?; - self.type_of_attribute(&Type::Model(_R(model).into()), mapped, scope) - .map(|it| _T!(it)) + let type_ = self.type_of_attribute(&Type::Model(_R(model).into()), mapped, scope)?; + let type_ = Index::wrap_in_container(type_, |it| Type::List(ListElement::Occupied(_T!(it)))); + Some(_T!(type_)) } "lambda" => { // (lambda (lambda_parameters)? body: (_)) diff --git a/testing/fixtures/python_types/bakery/models/models.py b/testing/fixtures/python_types/bakery/models/models.py index 43a68ca..d4c6efd 100644 --- a/testing/fixtures/python_types/bakery/models/models.py +++ b/testing/fixtures/python_types/bakery/models/models.py @@ -116,7 +116,7 @@ class Wine(models.Model): make = fields.Char() value = fields.Float() - def _test(self): + def _test_read_group(self): domain = [] for name, make, value in self._read_group(domain, ['name', 'make'], ['value:sum']): #^type PyBuiltin("str") @@ -125,6 +125,10 @@ def _test(self): value #^type PyBuiltin("float") + def _test_mapped(self): + foo = self.mapped('make') + #^type List(PyBuiltin("str")) + def test_grouped(self): for name, records in self.grouped('name').items(): #^type PyBuiltin("str")