From 3951d0d3d7439cc0595d75d4f9ebe75f38b69629 Mon Sep 17 00:00:00 2001 From: Mihai Bors Date: Thu, 15 Jan 2026 21:47:38 +0100 Subject: [PATCH 1/9] [IMP] Implement Phase 1 of Go To References (#466) --- server/src/features/references.rs | 108 ++++++++++++++++++++++++++++-- server/tests/test_get_symbol.rs | 54 ++++++++++++++- server/tests/test_utils.rs | 12 ++++ 3 files changed, 168 insertions(+), 6 deletions(-) diff --git a/server/src/features/references.rs b/server/src/features/references.rs index e568e660..47ba13f9 100644 --- a/server/src/features/references.rs +++ b/server/src/features/references.rs @@ -1,19 +1,117 @@ use std::{cell::RefCell, path::PathBuf, rc::Rc}; use lsp_types::{Location, Range}; +use ruff_python_ast::visitor::{walk_expr, walk_stmt, Visitor}; +use ruff_python_ast::{Expr, Stmt}; +use ruff_text_size::{Ranged, TextRange}; -use crate::{constants::SymType, core::{file_mgr::{FileInfo, FileMgr}, symbols::symbol::Symbol}, features::xml_ast_utils::{XmlAstResult, XmlAstUtils}, threads::SessionInfo, utils::PathSanitizer}; - +use crate::{constants::SymType, core::{file_mgr::{FileInfo, FileMgr}, symbols::symbol::Symbol}, features::ast_utils::AstUtils, features::xml_ast_utils::{XmlAstResult, XmlAstUtils}, threads::SessionInfo, utils::PathSanitizer}; pub struct ReferenceFeature { } +/// Visitor to find all Name expressions in an AST +struct NameFinderVisitor<'a> { + target_name: &'a str, + matches: Vec, +} + +impl<'a> NameFinderVisitor<'a> { + fn new(target_name: &'a str) -> Self { + Self { + target_name, + matches: Vec::new(), + } + } + + fn find_all_names(stmts: &[Stmt], target_name: &str) -> Vec { + let mut visitor = NameFinderVisitor::new(target_name); + for stmt in stmts { + visitor.visit_stmt(stmt); + } + visitor.matches + } +} + +impl<'a> Visitor<'a> for NameFinderVisitor<'a> { + fn visit_expr(&mut self, expr: &'a Expr) { + if let Expr::Name(name_expr) = expr { + if name_expr.id.as_str() == self.target_name { + self.matches.push(name_expr.range()); + } + } + walk_expr(self, expr); + } + + fn visit_stmt(&mut self, stmt: &'a Stmt) { + walk_stmt(self, stmt); + } +} + impl ReferenceFeature { - pub fn get_references(_session: &mut SessionInfo, _file_symbol: &Rc>, _file_info: &Rc>, _line: u32, _character: u32) -> Option> { - // Implementation for getting references - None + /// Basic find all references implementation + /// Same-file references only, local vars, params, methods + /// TODO: Cross-file references within module + /// TODO: All files within workspace + /// TODO: Odoo specific (XML field refs, string-based model refs) + pub fn get_references(session: &mut SessionInfo, file_symbol: &Rc>, file_info: &Rc>, line: u32, character: u32) -> Option> { + let offset = file_info.borrow().position_to_offset(line, character); + + let (analyse_ast_result, _range, _call_expr) = AstUtils::get_symbols(session, file_symbol, file_info, offset as u32); + + if analyse_ast_result.evaluations.is_empty() { + return None; + } + + let eval = &analyse_ast_result.evaluations[0]; + let target_symbol = eval.symbol.get_symbol_as_weak(session, &mut None, &mut vec![], None); + let target_symbol_rc = target_symbol.weak.upgrade()?; + + let symbol_name = target_symbol_rc.borrow().name().to_string(); + + // find all Name expressions with the same name + let file_info_ast = file_info.borrow().file_info_ast.clone(); + let file_info_ast_borrow = file_info_ast.borrow(); + let stmts = file_info_ast_borrow.get_stmts()?; + + let name_matches = NameFinderVisitor::find_all_names(stmts, &symbol_name); + + if name_matches.is_empty() { + return None; + } + + let file_path = file_symbol.borrow().paths()[0].clone(); + let mut locations = Vec::new(); + + for match_range in name_matches { + // infer name + let match_offset = match_range.start().to_u32(); + let scope = Symbol::get_scope_symbol(file_symbol.clone(), match_offset, false); + AstUtils::build_scope(session, &scope); + + let inferred = Symbol::infer_name(&mut session.sync_odoo, &scope, &symbol_name, Some(match_offset)); + + // do any of the inferred symbols match our target symbol + let refers_to_same_symbol = inferred.symbols.iter().any(|sym| { + Rc::ptr_eq(sym, &target_symbol_rc) + }); + + if refers_to_same_symbol { + let range = session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, &file_path, &match_range); + locations.push(Location { + uri: FileMgr::pathname2uri(&file_path), + range, + }); + } + } + + if locations.is_empty() { + None + } else { + Some(locations) + } } pub fn get_references_xml(session: &mut SessionInfo, file_symbol: &Rc>, file_info: &Rc>, line: u32, character: u32) -> Option> { diff --git a/server/tests/test_get_symbol.rs b/server/tests/test_get_symbol.rs index 653cd847..1675f131 100644 --- a/server/tests/test_get_symbol.rs +++ b/server/tests/test_get_symbol.rs @@ -254,6 +254,7 @@ fn test_definition() { // check that one of the phone_code_locs is the same as the phone_code field assert!(phone_code_locs.iter().any(|loc| loc.target_range == file_mgr.borrow().text_range_to_range(&mut session, &phone_code_file, phone_code_field_sym.borrow().range())), "Expected phone_code to be at the same location as the field"); } + #[test] fn test_model_subscription() { // Setup: Get the symbol for BaseTestModel and verify its existence @@ -277,4 +278,55 @@ fn test_model_subscription() { "Resolving a subscript of a model should include the model symbol itself. Expected to find BaseTestModel symbol among resolved symbols of `partner = self.search([], limit=2)[-1:]`" ) -} \ No newline at end of file +} + +#[test] +fn test_references() { + // setup + let mut odoo = setup::setup::setup_server(true); + let test_addons_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests").join("data").join("addons"); + let module1_test_file = test_addons_path.join("module_1").join("models").join("base_test_models.py").sanitize(); + + // test file exists + assert!(PathBuf::from(&module1_test_file).exists(), "Test file does not exist: {}", module1_test_file); + let mut session = setup::setup::create_session(&mut odoo); + + let file_mgr = session.sync_odoo.get_file_mgr(); + let m1_tf_file_info = file_mgr.borrow().get_file_info(&module1_test_file).unwrap(); + let Some(m1_tf_file_symbol) = SyncOdoo::get_symbol_of_opened_file( + &mut session, + &PathBuf::from(&module1_test_file) + ) else { + panic!("Failed to get file symbol"); + }; + + // Test references for BaseTestModel class + // BaseTestModel is referenced at: + // - Line 34 (0-indexed: 33): BaseOtherName = BaseTestModel + // - Line 35 (0-indexed: 34): baseInstance1 = BaseTestModel() + // - Line 37 (0-indexed: 36): ref_funcBase1 = BaseTestModel.get_test_int + let base_test_model_refs = test_utils::get_reference_locs(&mut session, &m1_tf_file_symbol, &m1_tf_file_info, 33, 17); + assert!(base_test_model_refs.len() >= 3, "Expected at least 3 references for BaseTestModel, got {}", base_test_model_refs.len()); + + // all refs should be in the same file + for loc in &base_test_model_refs { + assert_eq!(loc.uri.to_file_path().unwrap().sanitize(), module1_test_file, "Expected all references to be in the same file"); + } + + // Test references for 'var' variable in for_func method + // var is defined on line 21 (0-indexed: 20) and used on line 22 (0-indexed: 21) + let var_refs = test_utils::get_reference_locs(&mut session, &m1_tf_file_symbol, &m1_tf_file_info, 21, 18); + assert!(var_refs.len() >= 1, "Expected at least 1 reference for 'var', got {}", var_refs.len()); + + // Test references for CONSTANT_1 + // CONSTANT_1 is imported on line 2 (0-indexed: 1) and used on line 18 (0-indexed: 17) + // " return CONSTANT_1" - CONSTANT_1 starts at character 15 + let constant_refs = test_utils::get_reference_locs(&mut session, &m1_tf_file_symbol, &m1_tf_file_info, 17, 15); + assert!(constant_refs.len() >= 1, "Expected at least 1 reference for CONSTANT_1, got {}", constant_refs.len()); + + // Test references for 'self' parameter + // self is used multiple times within methods + let self_refs = test_utils::get_reference_locs(&mut session, &m1_tf_file_symbol, &m1_tf_file_info, 13, 8); + // self should have multiple references within the get_test_int method (lines 14, 15) + assert!(self_refs.len() >= 2, "Expected at least 2 references for 'self' in get_test_int method, got {}", self_refs.len()); +} diff --git a/server/tests/test_utils.rs b/server/tests/test_utils.rs index 1078a877..d19beb74 100644 --- a/server/tests/test_utils.rs +++ b/server/tests/test_utils.rs @@ -169,3 +169,15 @@ pub fn get_resolved_symbols_at_position( .filter_map(|ev| ev.upgrade_weak()) .collect() } + +/// Helper to get reference locations at a given (line, character) +pub fn get_reference_locs(session: &mut SessionInfo, f_sym: &Rc>, f_info: &Rc>, line: u32, character: u32) -> Vec { + let locations = odoo_ls_server::features::references::ReferenceFeature::get_references( + session, + f_sym, + f_info, + line, + character, + ); + locations.unwrap_or_default() +} From 7052fde63e5a64433eeda3e92ae8f4e0220f1d81 Mon Sep 17 00:00:00 2001 From: fda-odoo Date: Tue, 3 Feb 2026 23:28:53 +0100 Subject: [PATCH 2/9] [IMP] refactor definition to get symbol and build location by after --- server/src/core/evaluation.rs | 57 ++++- server/src/core/odoo.rs | 4 + server/src/features/definition.rs | 195 +++++++++------- server/src/features/references.rs | 377 +++++++++++++++++++++++------- server/tests/test_get_symbol.rs | 4 +- 5 files changed, 461 insertions(+), 176 deletions(-) diff --git a/server/src/core/evaluation.rs b/server/src/core/evaluation.rs index a202fd31..92994561 100644 --- a/server/src/core/evaluation.rs +++ b/server/src/core/evaluation.rs @@ -2,7 +2,8 @@ use itertools::Itertools; use itertools::FoldWhile::{Continue, Done}; use ruff_python_ast::{Arguments, Expr, ExprCall, Identifier, Number, Operator, Parameter, UnaryOp}; use ruff_text_size::{Ranged, TextRange, TextSize}; -use lsp_types::{Diagnostic, Position, Range}; +use lsp_types::{Diagnostic, Location, Position, Range}; +use tracing::error; use weak_table::traits::WeakElement; use std::cmp::{max, min}; use std::collections::{HashMap, HashSet}; @@ -688,6 +689,7 @@ impl Evaluation { let mut evals = vec![]; let mut diagnostics = vec![]; let module = parent.borrow().find_module(); + let mut found_one_reference = false; match ast { ExprOrIdent::Expr(Expr::StringLiteral(expr)) => { @@ -1007,6 +1009,9 @@ impl Evaluation { Some(ContextValue::SYMBOL(s)) => s.clone(), _ => Weak::new() }; + if session.sync_odoo.evaluation_search.is_some() { + Evaluation::search_reference_in_arg(session, expr, &parent); + } if is_in_validation { let on_instance = base_sym_weak_eval.context.get(&S!("is_attr_of_instance")).map(|v| v.as_bool()); call_argument_diagnostics.last_mut().unwrap().extend(Evaluation::validate_call_arguments(session, @@ -1266,11 +1271,13 @@ impl Evaluation { } }, ExprOrIdent::Expr(Expr::BinOp(operator)) => { - match operator.op { - Operator::Add => { - - }, - _ => {} + if odoo.evaluation_search.is_some() { + match operator.op { + Operator::Add | Operator::Mult | Operator::Sub | Operator::Div | Operator::BitAnd | Operator::BitOr | Operator::BitXor |Operator::FloorDiv | Operator::LShift | Operator::MatMult | Operator::Mod | Operator::Pow | Operator::RShift => { + Evaluation::eval_from_ast(session, &operator.left, parent.clone(), max_infer, false, required_dependencies); + Evaluation::eval_from_ast(session, &operator.right, parent.clone(), max_infer, false, required_dependencies); + } + } } }, ExprOrIdent::Expr(Expr::If(if_expr)) => { @@ -1302,6 +1309,9 @@ impl Evaluation { value: None, range: Some(unary_operator.range()), }); + if odoo.evaluation_search.is_some() { //Still evaluate if we are searching for something + Evaluation::eval_from_ast(session, &unary_operator.operand, parent.clone(), max_infer, for_annotation, required_dependencies); + } break 'u_op_block }, }; @@ -1351,6 +1361,29 @@ impl Evaluation { } _ => {} } + if let Some(evaluation_search) = session.sync_odoo.evaluation_search.as_ref() { + for eval in evals.iter() { + if eval.symbol.sym.is_weak() && let Some(weak) = eval.symbol.sym.as_weak().weak.upgrade() { + if Rc::ptr_eq(&weak, evaluation_search) { + if found_one_reference { + //if we have multiple matches, it means that that ast can reference it multiple times, but we only want to know if that ast matches or not + continue; + } + let file = parent.borrow().get_file().unwrap().upgrade().unwrap(); + let file_info = session.sync_odoo.get_file_mgr().borrow().get_file_info(&file.borrow().paths()[0]); + if let Some(file_info) = file_info { + found_one_reference = true; + let range= ast.range(); + let tranformed_range = file_info.borrow().text_range_to_range(&range, session.sync_odoo.encoding); + session.sync_odoo.evaluation_locations.push(Location { + uri: FileMgr::pathname2uri(&file.borrow().paths().first().unwrap()), + range: tranformed_range, + }); + } + } + } + } + } AnalyzeAstResult { evaluations: evals, diagnostics } } @@ -1735,6 +1768,18 @@ impl Evaluation { } diagnostics } + + fn search_reference_in_arg(session: &mut SessionInfo, expr_call: &ExprCall, parent: &Rc>) { + for arg in expr_call.arguments.args.iter() { + if arg.is_starred_expr() { + continue; + } + let (_, diags) = Evaluation::eval_from_ast(session, arg, parent.clone(), &expr_call.range.start(), false, &mut vec![]); + } + for arg in expr_call.arguments.keywords.iter() { + let (_, diags) = Evaluation::eval_from_ast(session, &arg.value, parent.clone(), &expr_call.range.start(), false, &mut vec![]); + } + } } impl EvaluationSymbol { diff --git a/server/src/core/odoo.rs b/server/src/core/odoo.rs index c6f24b15..74ed0151 100644 --- a/server/src/core/odoo.rs +++ b/server/src/core/odoo.rs @@ -99,6 +99,8 @@ pub struct SyncOdoo { pub capabilities: lsp_types::ClientCapabilities, pub encoding: PositionEncoding, pub opened_files: Vec, + pub evaluation_search: Option>>, //If set, any evaluation will be check against this value. If evaluation matches, location is kept in evaluation_locations + pub evaluation_locations: Vec, pub test_mode: bool, } @@ -145,6 +147,8 @@ impl SyncOdoo { capabilities: lsp_types::ClientCapabilities::default(), encoding: PositionEncoding::Utf16, opened_files: vec![], + evaluation_search: None, + evaluation_locations: vec![], test_mode: false, }; diff --git a/server/src/features/definition.rs b/server/src/features/definition.rs index aef6111c..30bf7718 100644 --- a/server/src/features/definition.rs +++ b/server/src/features/definition.rs @@ -10,6 +10,7 @@ use crate::core::file_mgr::{FileInfo, FileMgr}; use crate::core::odoo::SyncOdoo; use crate::core::python_odoo_builder::MAGIC_FIELDS; use crate::core::symbols::symbol::Symbol; +use crate::core::xml_data::OdooData; use crate::features::ast_utils::AstUtils; use crate::features::features_utils::FeaturesUtils; use crate::features::xml_ast_utils::{XmlAstResult, XmlAstUtils}; @@ -17,11 +18,22 @@ use crate::{S, oyarn}; use crate::threads::SessionInfo; use crate::utils::PathSanitizer as _; +pub enum DefinitionSourceType { + Symbol(Rc>), + OdooData(OdooData), + Module(Rc>), +} + +pub struct DefinitionSource { + pub source: DefinitionSourceType, + pub origin_selection_range: Option, +} + pub struct DefinitionFeature {} impl DefinitionFeature { - fn check_for_domain_field(session: &mut SessionInfo, eval: &Evaluation, file_symbol: &Rc>, call_expr: &Option, offset: usize, links: &mut Vec) -> bool { + fn check_for_domain_field(session: &mut SessionInfo, eval: &Evaluation, file_symbol: &Rc>, call_expr: &Option, offset: usize, sources: &mut Vec) -> bool { let (field_name, field_range) = if let Some(eval_value) = eval.value.as_ref() { if let EvaluationValue::CONSTANT(Expr::StringLiteral(expr)) = eval_value { (expr.value.to_string(), expr.range) @@ -36,22 +48,21 @@ impl DefinitionFeature { let string_domain_fields = FeaturesUtils::find_argument_symbols( session, Symbol::get_scope_symbol(file_symbol.clone(), offset as u32, false), module, &field_name, call_expr, offset, field_range ); + let mut domain_found = false; string_domain_fields.iter().for_each(|(field, field_range)|{ - if let Some(file_sym) = field.borrow().get_file().and_then(|file_sym_weak| file_sym_weak.upgrade()){ - let path = file_sym.borrow().paths()[0].clone(); - let range = session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, &path, &field.borrow().range()); - links.push(LocationLink{ - origin_selection_range: Some(session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, file_symbol.borrow().paths().first().as_ref().unwrap(), &field_range)), - target_uri: FileMgr::pathname2uri(&path), - target_selection_range: range, - target_range: range, + if let Some(_) = field.borrow().get_file().and_then(|file_sym_weak| file_sym_weak.upgrade()){ + domain_found = true; + sources.push(DefinitionSource { + source: + DefinitionSourceType::Symbol(field.clone()), + origin_selection_range: Some(session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, file_symbol.borrow().paths().first().as_ref().unwrap(), &field_range)) }); } }); - string_domain_fields.len() > 0 + domain_found } - fn check_for_model_string(session: &mut SessionInfo, eval: &Evaluation, file_symbol: &Rc>, links: &mut Vec) -> bool { + fn check_for_model_string(session: &mut SessionInfo, eval: &Evaluation, file_symbol: &Rc>, sources: &mut Vec) -> bool { let value = if let Some(eval_value) = eval.value.as_ref() { if let EvaluationValue::CONSTANT(Expr::StringLiteral(expr)) = eval_value { oyarn!("{}", expr.value.to_string()) @@ -76,22 +87,16 @@ impl DefinitionFeature { continue; // if we are already on the class, skip, unless it is the only result } } - if let Some(model_file_sym) = class_symbol.get_file().and_then(|model_file_sym_weak| model_file_sym_weak.upgrade()){ - let path = model_file_sym.borrow().get_symbol_first_path(); - let range = session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, &path, &class_symbol.range()); - model_found = true; - links.push(LocationLink{ - origin_selection_range: eval.range.map(|r| session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, file_symbol.borrow().paths().first().as_ref().unwrap(), &r)), - target_uri: FileMgr::pathname2uri(&path), - target_selection_range: range, - target_range: range, - }); - } + model_found = true; + sources.push(DefinitionSource { + source: DefinitionSourceType::Symbol(class_symbol_rc.clone()), + origin_selection_range: eval.range.map(|r| session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, file_symbol.borrow().paths().first().as_ref().unwrap(), &r)) + }); } model_found } - fn check_for_module_string(session: &mut SessionInfo, eval: &Evaluation, file_symbol: &Rc>, file_path: &String, links: &mut Vec) -> bool { + fn check_for_module_string(session: &mut SessionInfo, eval: &Evaluation, file_symbol: &Rc>, file_path: &String, sources: &mut Vec) -> bool { if file_symbol.borrow().typ() != SymType::PACKAGE(PackageType::MODULE) || !file_path.ends_with("__manifest__.py") { // If not on manifest, we don't check for modules return false; @@ -108,17 +113,14 @@ impl DefinitionFeature { let Some(module) = session.sync_odoo.modules.get(&oyarn!("{}", value)).and_then(|m| m.upgrade()) else { return false; }; - let path = PathBuf::from(module.borrow().paths()[0].clone()).join("__manifest__.py").sanitize(); - links.push(LocationLink{ + sources.push(DefinitionSource{ + source: DefinitionSourceType::Module(module.clone()), origin_selection_range: None, - target_uri: FileMgr::pathname2uri(&path), - target_selection_range: Range::default(), - target_range: Range::default(), }); true } - fn check_for_xml_id_string(session: &mut SessionInfo, eval: &Evaluation, file_symbol: &Rc>, links: &mut Vec) -> bool { + fn check_for_xml_id_string(session: &mut SessionInfo, eval: &Evaluation, file_symbol: &Rc>, sources: &mut Vec) -> bool { let value = if let Some(eval_value) = eval.value.as_ref() { if let EvaluationValue::CONSTANT(Expr::StringLiteral(expr)) = eval_value { oyarn!("{}", expr.value.to_string()) @@ -134,20 +136,18 @@ impl DefinitionFeature { let file = xml_id.get_file_symbol(); if let Some(file) = file { if let Some(file) = file.upgrade() { - let range = session.sync_odoo.get_file_mgr().borrow().std_range_to_range(session, &file.borrow().paths()[0], &xml_id.get_range()); xml_found = true; - links.push(LocationLink { - origin_selection_range: eval.range.map(|r| session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, file_symbol.borrow().paths().first().as_ref().unwrap(), &r)), - target_uri: FileMgr::pathname2uri(&file.borrow().paths()[0]), - target_range: range, - target_selection_range: range }); + sources.push(DefinitionSource{ + source: DefinitionSourceType::OdooData(xml_id.clone()), + origin_selection_range: eval.range.map(|r| session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, file_symbol.borrow().paths().first().as_ref().unwrap(), &r)) + }); } } } xml_found } - fn check_for_compute_string(session: &mut SessionInfo, eval: &Evaluation, file_symbol: &Rc>, call_expr: &Option, offset: usize, links: &mut Vec) -> bool { + fn check_for_compute_string(session: &mut SessionInfo, eval: &Evaluation, file_symbol: &Rc>, call_expr: &Option, offset: usize, sources: &mut Vec) -> bool { let value = if let Some(eval_value) = eval.value.as_ref() { if let EvaluationValue::CONSTANT(Expr::StringLiteral(expr)) = eval_value { expr.value.to_string() @@ -161,22 +161,20 @@ impl DefinitionFeature { let method_symbols = FeaturesUtils::find_kwarg_methods_symbols( session, Symbol::get_scope_symbol(file_symbol.clone(), offset as u32, false), file_symbol.borrow().find_module(), &value, call_expr, &offset ); + let mut method_found = false; method_symbols.iter().for_each(|field|{ if let Some(file_sym) = field.borrow().get_file().and_then(|file_sym_weak| file_sym_weak.upgrade()){ - let path = file_sym.borrow().paths()[0].clone(); - let range = session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, &path, &field.borrow().range()); - links.push(LocationLink{ - origin_selection_range: eval.range.map(|r| session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, file_symbol.borrow().paths().first().as_ref().unwrap(), &r)), - target_uri: FileMgr::pathname2uri(&path), - target_selection_range: range, - target_range: range, + method_found = true; + sources.push(DefinitionSource { + source: DefinitionSourceType::Symbol(field.clone()), + origin_selection_range: eval.range.map(|r| session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, file_symbol.borrow().paths().first().as_ref().unwrap(), &r)) }); } }); - method_symbols.len() > 0 + method_found } - pub fn add_display_name_compute_methods(session: &mut SessionInfo, links: &mut Vec, expr: &ExprOrIdent, file_symbol: &Rc>, offset: usize) { + pub fn add_display_name_compute_methods(session: &mut SessionInfo, sources: &mut Vec, expr: &ExprOrIdent, file_symbol: &Rc>, offset: usize) { // now we want `_compute_display_name` definition(s) // we need the symbol of the model/ then we run get member symbol // to do that, we need the expr, match it to attribute, get the value, get its evals @@ -208,31 +206,29 @@ impl DefinitionFeature { } else { Range::default() }; - links.push(LocationLink{ + sources.push(DefinitionSource { + source: DefinitionSourceType::Symbol(symbol.clone()), origin_selection_range: None, - target_uri: FileMgr::pathname2uri(&full_path), - target_selection_range: range, - target_range: range, }); } } } } - pub fn get_location(session: &mut SessionInfo, + pub fn get_symbols(session: &mut SessionInfo, file_symbol: &Rc>, file_info: &Rc>, line: u32, character: u32 - ) -> Option { + ) -> Vec { let offset = file_info.borrow().position_to_offset(line, character, session.sync_odoo.encoding); let file_info_ast_clone = file_info.borrow().file_info_ast.clone(); let file_info_ast_ref = file_info_ast_clone.borrow(); let (analyse_ast_result, _range, expr, call_expr) = AstUtils::get_symbols(session, &file_info_ast_ref, file_symbol, offset as u32); if analyse_ast_result.evaluations.is_empty() { - return None; + return vec![]; } - let mut links = vec![]; + let mut definition_sources = vec![]; let mut evaluations = analyse_ast_result.evaluations.clone(); // Filter out magic fields let mut dislay_name_found = false; @@ -253,17 +249,17 @@ impl DefinitionFeature { eval_sym.borrow().range() != parent_sym.borrow().range() }); if let Some(expr) = expr && dislay_name_found { - DefinitionFeature::add_display_name_compute_methods(session, &mut links, &expr, file_symbol, offset); + DefinitionFeature::add_display_name_compute_methods(session, &mut definition_sources, &expr, file_symbol, offset); } drop(file_info_ast_ref); let mut index = 0; while index < evaluations.len() { let eval = evaluations[index].clone(); - if DefinitionFeature::check_for_domain_field(session, &eval, file_symbol, &call_expr, offset, &mut links) || - DefinitionFeature::check_for_compute_string(session, &eval, file_symbol,&call_expr, offset, &mut links) || - DefinitionFeature::check_for_module_string(session, &eval, file_symbol, &file_info.borrow().uri, &mut links) || - DefinitionFeature::check_for_model_string(session, &eval, file_symbol, &mut links) || - DefinitionFeature::check_for_xml_id_string(session, &eval, file_symbol, &mut links) { + if DefinitionFeature::check_for_domain_field(session, &eval, file_symbol, &call_expr, offset, &mut definition_sources) || + DefinitionFeature::check_for_compute_string(session, &eval, file_symbol,&call_expr, offset, &mut definition_sources) || + DefinitionFeature::check_for_module_string(session, &eval, file_symbol, &file_info.borrow().uri, &mut definition_sources) || + DefinitionFeature::check_for_model_string(session, &eval, file_symbol, &mut definition_sources) || + DefinitionFeature::check_for_xml_id_string(session, &eval, file_symbol, &mut definition_sources) { index += 1; continue; } @@ -276,36 +272,67 @@ impl DefinitionFeature { index += 1; continue; }; - if let Some(file) = symbol.borrow().get_file() { - //For import variable, we should take the next evaluation if we are at the same location than the offset, as the get_symbol will return the current import variable (special case as the definition is from outside the file) - if symbol.borrow().typ() == SymType::VARIABLE && symbol.borrow().as_variable().is_import_variable && Rc::ptr_eq(&file.upgrade().unwrap(), file_symbol) && symbol.borrow().has_range() && symbol.borrow().range().contains(TextSize::new(offset as u32)) { - evaluations.remove(index); - let symbol = symbol.borrow(); - let sym_eval = symbol.evaluations(); - if let Some(sym_eval) = sym_eval { - evaluations = [evaluations.clone(), sym_eval.clone()].concat(); - } - continue; - } - for path in file.upgrade().unwrap().borrow().paths().iter() { - let full_path = match file.upgrade().unwrap().borrow().typ() { - SymType::PACKAGE(_) => PathBuf::from(path).join(format!("__init__.py{}", file.upgrade().unwrap().borrow().as_package().i_ext())).sanitize(), - _ => path.clone() - }; - let range = if symbol.borrow().has_range() { - session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, &full_path, &symbol.borrow().range()) - } else { - Range::default() - }; - links.push(LocationLink{ - origin_selection_range: None, - target_uri: FileMgr::pathname2uri(&full_path), + definition_sources.push(DefinitionSource{ + source: DefinitionSourceType::Symbol(symbol.clone()), + origin_selection_range: None + }); + index += 1; + } + definition_sources + } + + pub fn definition_source_to_location(session: &mut SessionInfo, def: &DefinitionSource) -> Option { + match &def.source { + DefinitionSourceType::Symbol(symbol) => { + if let Some(file_symbol) = symbol.borrow().get_file().and_then(|file_sym_weak| file_sym_weak.upgrade()){ + let path = file_symbol.borrow().get_symbol_first_path(); + let range = session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, &path, &symbol.borrow().range()); + return Some(LocationLink{ + origin_selection_range: def.origin_selection_range.clone(), + target_uri: FileMgr::pathname2uri(&path), target_selection_range: range, target_range: range, }); } + }, + DefinitionSourceType::Module(module) => { + let path = PathBuf::from(module.borrow().paths()[0].clone()).join("__manifest__.py").sanitize(); + return Some(LocationLink{ + origin_selection_range: None, + target_uri: FileMgr::pathname2uri(&path), + target_selection_range: Range::default(), + target_range: Range::default(), + }); + }, + DefinitionSourceType::OdooData(xml_id) => { + let file = xml_id.get_file_symbol(); + if let Some(file) = file { + if let Some(file) = file.upgrade() { + let range = session.sync_odoo.get_file_mgr().borrow().std_range_to_range(session, &file.borrow().paths()[0], &xml_id.get_range()); + return Some(LocationLink { + origin_selection_range: def.origin_selection_range.clone(), + target_uri: FileMgr::pathname2uri(&file.borrow().paths()[0]), + target_range: range, + target_selection_range: range }); + } + } + } + } + None + } + + pub fn get_location(session: &mut SessionInfo, + file_symbol: &Rc>, + file_info: &Rc>, + line: u32, + character: u32 + ) -> Option { + let definitions_sources = DefinitionFeature::get_symbols(session, file_symbol, file_info, line, character); + let mut links = vec![]; + for def in definitions_sources.iter() { + if let Some(link) = DefinitionFeature::definition_source_to_location(session, def) { + links.push(link); } - index += 1; } Some(GotoDefinitionResponse::Link(links)) } diff --git a/server/src/features/references.rs b/server/src/features/references.rs index 47ba13f9..30b066e0 100644 --- a/server/src/features/references.rs +++ b/server/src/features/references.rs @@ -2,54 +2,21 @@ use std::{cell::RefCell, path::PathBuf, rc::Rc}; use lsp_types::{Location, Range}; use ruff_python_ast::visitor::{walk_expr, walk_stmt, Visitor}; -use ruff_python_ast::{Expr, Stmt}; -use ruff_text_size::{Ranged, TextRange}; +use ruff_python_ast::{Alias, Expr, Identifier, Stmt, StmtAnnAssign, StmtAssert, StmtAssign, StmtAugAssign, StmtClassDef, StmtIf, StmtMatch, StmtRaise, StmtReturn, StmtTry, StmtTypeAlias, StmtWith}; +use ruff_text_size::{Ranged, TextRange, TextSize}; -use crate::{constants::SymType, core::{file_mgr::{FileInfo, FileMgr}, symbols::symbol::Symbol}, features::ast_utils::AstUtils, features::xml_ast_utils::{XmlAstResult, XmlAstUtils}, threads::SessionInfo, utils::PathSanitizer}; +use crate::S; +use crate::constants::OYarn; +use crate::core::evaluation::{Evaluation, EvaluationSymbolPtr}; +use crate::core::odoo::SyncOdoo; +use crate::features::definition::{DefinitionFeature, DefinitionSourceType}; +use crate::{constants::SymType, core::{file_mgr::{FileInfo, FileMgr}, symbols::symbol::Symbol}, features::xml_ast_utils::{XmlAstResult, XmlAstUtils}, threads::SessionInfo, utils::PathSanitizer}; pub struct ReferenceFeature { } -/// Visitor to find all Name expressions in an AST -struct NameFinderVisitor<'a> { - target_name: &'a str, - matches: Vec, -} - -impl<'a> NameFinderVisitor<'a> { - fn new(target_name: &'a str) -> Self { - Self { - target_name, - matches: Vec::new(), - } - } - - fn find_all_names(stmts: &[Stmt], target_name: &str) -> Vec { - let mut visitor = NameFinderVisitor::new(target_name); - for stmt in stmts { - visitor.visit_stmt(stmt); - } - visitor.matches - } -} - -impl<'a> Visitor<'a> for NameFinderVisitor<'a> { - fn visit_expr(&mut self, expr: &'a Expr) { - if let Expr::Name(name_expr) = expr { - if name_expr.id.as_str() == self.target_name { - self.matches.push(name_expr.range()); - } - } - walk_expr(self, expr); - } - - fn visit_stmt(&mut self, stmt: &'a Stmt) { - walk_stmt(self, stmt); - } -} - impl ReferenceFeature { /// Basic find all references implementation /// Same-file references only, local vars, params, methods @@ -57,61 +24,101 @@ impl ReferenceFeature { /// TODO: All files within workspace /// TODO: Odoo specific (XML field refs, string-based model refs) pub fn get_references(session: &mut SessionInfo, file_symbol: &Rc>, file_info: &Rc>, line: u32, character: u32) -> Option> { - let offset = file_info.borrow().position_to_offset(line, character); + //We want to search for references of the definition, and not the current symbol. Let's use definition feature for that + let def_sources = DefinitionFeature::get_symbols(session, file_symbol, file_info, line, character); - let (analyse_ast_result, _range, _call_expr) = AstUtils::get_symbols(session, file_symbol, file_info, offset as u32); + let mut locations = Vec::new(); + for definition in def_sources.iter() { + match &definition.source { + DefinitionSourceType::Symbol(target_symbol) => { + let symbol_name = target_symbol.borrow().name().to_string(); - if analyse_ast_result.evaluations.is_empty() { - return None; - } + locations.extend(ReferenceFeature::references_in_file(session, file_symbol, file_info, &symbol_name, &target_symbol)); - let eval = &analyse_ast_result.evaluations[0]; - let target_symbol = eval.symbol.get_symbol_as_weak(session, &mut None, &mut vec![], None); - let target_symbol_rc = target_symbol.weak.upgrade()?; + //take arch and arch_eval dependents + for dep in file_symbol.borrow().dependents()[0].iter().take(2) { + if let Some(dep_set) = dep { + for dep_symbol_rc in dep_set.iter() { + let Some(Some(file)) = dep_symbol_rc.borrow().get_file().map(|x| x.upgrade()) else { //to be sure we are on a file + continue; + }; + let Some(dep_file_info) = session.sync_odoo.get_file_mgr().borrow().get_file_info(&file.borrow().paths()[0]) else { + continue; + }; + locations.extend(ReferenceFeature::references_in_file(session, &dep_symbol_rc, &dep_file_info, &symbol_name, &target_symbol)); + } + } + } + //If the symbol is a model, browse model dependents too + if target_symbol.borrow().typ() == SymType::CLASS { + if let Some(model_data) = target_symbol.borrow().as_class_sym()._model.as_ref() { + if let Some(model) = session.sync_odoo.models.get(&model_data.name).cloned() { + let dependents = model.borrow().dependents.clone(); + for dep_symbol_rc in dependents.iter() { + let Some(Some(file)) = dep_symbol_rc.borrow().get_file().map(|x| x.upgrade()) else { //to be sure we are on a file + continue; + }; + let Some(dep_file_info) = session.sync_odoo.get_file_mgr().borrow().get_file_info(&file.borrow().paths()[0]) else { + continue; + }; + locations.extend(ReferenceFeature::references_in_file(session, &dep_symbol_rc, &dep_file_info, &symbol_name, &target_symbol)); + } + } + } + } + }, + _ => continue, + } + } - let symbol_name = target_symbol_rc.borrow().name().to_string(); + if locations.is_empty() { + None + } else { + Some(locations) + } + } - // find all Name expressions with the same name + fn references_in_file(session: &mut SessionInfo, file_symbol: &Rc>, file_info: &Rc>, symbol_name: &String, target_symbol_rc: &Rc>) -> Vec { + SyncOdoo::process_rebuilds(session, false); let file_info_ast = file_info.borrow().file_info_ast.clone(); - let file_info_ast_borrow = file_info_ast.borrow(); - let stmts = file_info_ast_borrow.get_stmts()?; - - let name_matches = NameFinderVisitor::find_all_names(stmts, &symbol_name); + let mut visitor = ReferenceVisitor { + sym_stack: vec![], + }; + session.sync_odoo.evaluation_locations = vec![]; + session.sync_odoo.evaluation_search = Some(target_symbol_rc.clone()); + visitor.browse_file(session, file_symbol, file_info_ast.borrow().get_stmts().as_ref().unwrap()); + session.sync_odoo.evaluation_search = None; + std::mem::take(&mut session.sync_odoo.evaluation_locations) + } - if name_matches.is_empty() { - return None; + fn is_same_or_imported(session: &mut SessionInfo, symbol: &Rc>, target_symbol: &Rc>) -> bool { + if Rc::ptr_eq(symbol, target_symbol) { + return true; } - let file_path = file_symbol.borrow().paths()[0].clone(); - let mut locations = Vec::new(); - - for match_range in name_matches { - // infer name - let match_offset = match_range.start().to_u32(); - let scope = Symbol::get_scope_symbol(file_symbol.clone(), match_offset, false); - AstUtils::build_scope(session, &scope); - - let inferred = Symbol::infer_name(&mut session.sync_odoo, &scope, &symbol_name, Some(match_offset)); - - // do any of the inferred symbols match our target symbol - let refers_to_same_symbol = inferred.symbols.iter().any(|sym| { - Rc::ptr_eq(sym, &target_symbol_rc) - }); - - if refers_to_same_symbol { - let range = session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, &file_path, &match_range); - locations.push(Location { - uri: FileMgr::pathname2uri(&file_path), - range, - }); + // Check if symbol is an imported symbol that points to the target symbol + if symbol.borrow().typ() != SymType::VARIABLE { + return false; + } + if symbol.borrow().as_variable().is_import_variable { + if symbol.borrow().evaluations().as_ref().unwrap().is_empty() { + return false; + } + let file_symbol = symbol.borrow().get_file().unwrap().upgrade().unwrap(); + for eval in symbol.borrow().evaluations().as_ref().unwrap().iter() { + let eval_ptr = eval.symbol.get_symbol(session, &mut None, &mut vec![], Some(file_symbol.clone())); + match eval_ptr { + EvaluationSymbolPtr::WEAK(w) => { + if let Some(sym_upg) = w.weak.upgrade() { + return ReferenceFeature::is_same_or_imported(session, &sym_upg, target_symbol) + } + }, + _ => {continue;} + } } } - if locations.is_empty() { - None - } else { - Some(locations) - } + false } pub fn get_references_xml(session: &mut SessionInfo, file_symbol: &Rc>, file_info: &Rc>, line: u32, character: u32) -> Option> { @@ -165,4 +172,206 @@ impl ReferenceFeature { pub fn get_references_csv(_session: &mut SessionInfo, _file_symbol: &Rc>, _file_info: &Rc>, _line: u32, _character: u32) -> Option> { None } +} + +struct ReferenceVisitor { + sym_stack: Vec>>, +} + +impl ReferenceVisitor { + + pub fn browse_file(&mut self, session: &mut SessionInfo, file_symbol: &Rc>, stmts: &Vec) { + self.sym_stack.push(file_symbol.clone()); + self.visit_vec_stmt(session, stmts); + } + + pub fn visit_vec_stmt(&mut self, session: &mut SessionInfo, vec_ast: &Vec) { + for stmt in vec_ast.iter() { + match stmt { + Stmt::FunctionDef(f) => { + let sym = self.sym_stack.last().unwrap().borrow().get_positioned_symbol(&OYarn::from(f.name.to_string()), &f.range); + if let Some(sym) = sym { + self.sym_stack.push(sym); + self.visit_vec_stmt(session, &f.body); + self.sym_stack.pop(); + } + }, + Stmt::ClassDef(c) => { + self.visit_class_def(session, c); + }, + Stmt::Try(t) => { + self.visit_try(session, t); + }, + Stmt::Import(i) => { + self._resolve_import(session, None, &i.names, None, &i.range); + }, + Stmt::ImportFrom(i) => { + self._resolve_import(session, i.module.as_ref(), &i.names, Some(i.level), &i.range); + }, + Stmt::Assign(a) => { + self.visit_assign(session, a); + }, + Stmt::AnnAssign(a) => { + self.visit_ann_assign(session, a); + }, + Stmt::Expr(e) => { + self.visit_expr(session, &e.value, &e.value.start()); + }, + Stmt::If(i) => { + self.visit_if(session, i); + }, + Stmt::Break(_) => {}, + Stmt::Continue(_) => {}, + Stmt::Delete(d) => { + for target in d.targets.iter() { + self.visit_expr(session, target, &target.start()); + } + }, + Stmt::For(f) => { + self.visit_expr(session, &f.target, &f.target.start()); + self.visit_vec_stmt(session, &f.body); + self.visit_vec_stmt(session, &f.orelse); + }, + Stmt::While(w) => { + self.visit_expr(session, &w.test, &w.test.start()); + self.visit_vec_stmt(session, &w.body); + self.visit_vec_stmt(session, &w.orelse); + }, + Stmt::Return(stmt_return) => self.visit_return_stmt(session, stmt_return), + Stmt::AugAssign(stmt_aug_assign) => self.visit_aug_assign(session, stmt_aug_assign), + Stmt::TypeAlias(stmt_type_alias) => self.visit_type_alias(session, stmt_type_alias), + Stmt::With(stmt_with) => self.visit_with(session, stmt_with), + Stmt::Match(stmt_match) => self.visit_match(session, stmt_match), + Stmt::Raise(stmt_raise) => self.visit_raise(session, stmt_raise), + Stmt::Assert(stmt_assert) => self.visit_assert(session, stmt_assert), + Stmt::Global(_) => {}, + Stmt::Nonlocal(_) => {}, + Stmt::Pass(_) => {}, + Stmt::IpyEscapeCommand(_) => {}, + } + }; + } + + fn _resolve_import(&mut self, session: &mut SessionInfo, _from_stmt: Option<&Identifier>, name_aliases: &[Alias], _level: Option, _range: &TextRange) { + let file_symbol = self.sym_stack[0].borrow().get_file(); + let file_symbol = file_symbol.expect("file symbol not found").upgrade().expect("unable to upgrade file symbol"); + let Some(eval_search) = session.sync_odoo.evaluation_search.clone() else { + return; + }; + for alias in name_aliases.iter() { + if alias.name.id == "*" { + continue; + } + let var_name = if alias.asname.is_none() { + S!(alias.name.split(".").next().unwrap()) + } else { + alias.asname.as_ref().unwrap().clone().to_string() + }; + let variable = self.sym_stack.last().unwrap().borrow().get_positioned_symbol(&OYarn::from(var_name), &alias.range); + if let Some(variable) = variable { + for evaluation in variable.borrow().evaluations().as_ref().unwrap().iter() { + let eval_sym = evaluation.symbol.get_symbol(session, &mut None, &mut vec![], Some(file_symbol.clone())); + match eval_sym { + EvaluationSymbolPtr::WEAK(w) => { + if let Some(symbol) = w.weak.upgrade() { + if Rc::ptr_eq(&symbol, &eval_search) { + let range = session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, &file_symbol.borrow().paths()[0], &alias.range); + session.sync_odoo.evaluation_locations.push(Location { + uri: FileMgr::pathname2uri(&file_symbol.borrow().paths()[0]), + range: range, + }); + } + } + }, + _ => { + panic!("Internal error: evaluated has invalid evaluationType"); + } + } + } + } + } + } + + fn visit_class_def(&mut self, session: &mut SessionInfo, c: &StmtClassDef) { + let sym = self.sym_stack.last().unwrap().borrow().get_positioned_symbol(&OYarn::from(c.name.to_string()), &c.range); + if let Some(sym) = sym { + self.sym_stack.push(sym); + self.visit_vec_stmt(session, &c.body); + self.sym_stack.pop(); + } + } + + fn visit_expr(&mut self, session: &mut SessionInfo, expr: &Expr, max_infer: &TextSize) { + Evaluation::eval_from_ast(session, expr, self.sym_stack.last().unwrap().clone(), max_infer, false, &mut vec![]); + } + + fn visit_assert(&mut self, session: &mut SessionInfo, stmt_assert: &StmtAssert) { + self.visit_expr(session, &stmt_assert.test, &stmt_assert.range.start()); + if let Some(msg) = stmt_assert.msg.as_ref() { + self.visit_expr(session, msg, &stmt_assert.range.start()); + } + } + + fn visit_raise(&mut self, session: &mut SessionInfo, stmt_raise: &StmtRaise) { + if let Some(exc) = stmt_raise.exc.as_ref() { + self.visit_expr(session, exc, &stmt_raise.range.start()); + } + } + + fn visit_match(&mut self, session: &mut SessionInfo, stmt_match: &StmtMatch) { + self.visit_expr(session, &stmt_match.subject, &stmt_match.range.start()); + for case in stmt_match.cases.iter() { + if let Some(guard) = case.guard.as_ref() { + self.visit_expr(session, guard, &case.pattern.start()); + } + self.visit_vec_stmt(session, &case.body); + } + } + + fn visit_type_alias(&mut self, session: &mut SessionInfo, stmt_type_alias: &StmtTypeAlias) { + self.visit_expr(session, &stmt_type_alias.value, &stmt_type_alias.range.start()); + } + + fn visit_aug_assign(&mut self, session: &mut SessionInfo, assign: &StmtAugAssign) { + self.visit_expr(session, &assign.value, &assign.range.start()); + } + + fn visit_return_stmt(&mut self, session: &mut SessionInfo, stmt_return: &StmtReturn) { + if let Some(value) = stmt_return.value.as_ref() { + self.visit_expr(session, value, &stmt_return.range.start()); + } + } + + fn visit_if(&mut self, session: &mut SessionInfo, node: &StmtIf) { + self.visit_expr(session, &node.test, &node.test.start()); + self.visit_vec_stmt(session, &node.body); + for elses in node.elif_else_clauses.iter() { + if let Some(test) = &elses.test { + self.visit_expr(session, test, &test.start()); + } + self.visit_vec_stmt(session, &elses.body); + } + } + + fn visit_with(&mut self, session: &mut SessionInfo, stmt_with: &StmtWith) { + for item in stmt_with.items.iter() { + self.visit_expr(session, &item.context_expr, &stmt_with.range.start()); + } + self.visit_vec_stmt(session, &stmt_with.body); + } + + fn visit_assign(&mut self, session: &mut SessionInfo, assign: &StmtAssign) { + self.visit_expr(session, &assign.value, &assign.range.start()); + } + + fn visit_ann_assign(&mut self, session: &mut SessionInfo, assign: &StmtAnnAssign) { + if let Some(value) = assign.value.as_ref() { + self.visit_expr(session, value, &assign.range.start()); + } + } + + fn visit_try(&mut self, session: &mut SessionInfo, node: &StmtTry) { + //TODO handle handlers of try + self.visit_vec_stmt(session, &node.body); + } } \ No newline at end of file diff --git a/server/tests/test_get_symbol.rs b/server/tests/test_get_symbol.rs index 1675f131..39d3c62d 100644 --- a/server/tests/test_get_symbol.rs +++ b/server/tests/test_get_symbol.rs @@ -283,13 +283,13 @@ fn test_model_subscription() { #[test] fn test_references() { // setup - let mut odoo = setup::setup::setup_server(true); + let (mut odoo, config) = setup::setup::setup_server(true); let test_addons_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests").join("data").join("addons"); let module1_test_file = test_addons_path.join("module_1").join("models").join("base_test_models.py").sanitize(); // test file exists assert!(PathBuf::from(&module1_test_file).exists(), "Test file does not exist: {}", module1_test_file); - let mut session = setup::setup::create_session(&mut odoo); + let mut session = setup::setup::create_init_session(&mut odoo, config); let file_mgr = session.sync_odoo.get_file_mgr(); let m1_tf_file_info = file_mgr.borrow().get_file_info(&module1_test_file).unwrap(); From 3a33f4ed8b2e8d7a8fff47fadaf16dd088bb1f67 Mon Sep 17 00:00:00 2001 From: fda-odoo Date: Tue, 17 Feb 2026 18:37:47 +0100 Subject: [PATCH 3/9] [IMP] GotoDeclaration --- server/src/core/odoo.rs | 33 ++- server/src/features/declaration.rs | 122 +++++++++++ server/src/features/definition.rs | 315 +--------------------------- server/src/features/goto_utils.rs | 319 +++++++++++++++++++++++++++++ server/src/features/mod.rs | 2 + server/src/features/references.rs | 8 +- server/src/server.rs | 11 +- server/src/threads.rs | 11 +- 8 files changed, 497 insertions(+), 324 deletions(-) create mode 100644 server/src/features/declaration.rs create mode 100644 server/src/features/goto_utils.rs diff --git a/server/src/core/odoo.rs b/server/src/core/odoo.rs index 74ed0151..62ae86b3 100644 --- a/server/src/core/odoo.rs +++ b/server/src/core/odoo.rs @@ -5,6 +5,7 @@ use crate::core::file_mgr::AstType; use crate::core::module_load_order::sort_by_load_order; use crate::core::xml_data::OdooData; use crate::core::xml_validation::XmlValidator; +use crate::features::declaration::DeclarationFeature; use crate::features::document_symbols::DocumentSymbolFeature; use crate::features::references::ReferenceFeature; use crate::features::workspace_symbols::WorkspaceSymbolFeature; @@ -21,7 +22,7 @@ use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use lsp_server::{ErrorCode, RequestId, ResponseError}; use lsp_types::notification::{Notification, Progress}; -use lsp_types::request::WorkDoneProgressCreate; +use lsp_types::request::{GotoDeclarationResponse, WorkDoneProgressCreate}; use lsp_types::*; use request::{RegisterCapability, Request, WorkspaceConfiguration}; use ruff_source_file::PositionEncoding; @@ -1430,10 +1431,18 @@ impl Odoo { } pub fn handle_goto_definition(session: &mut SessionInfo, params: GotoDefinitionParams) -> Result, ResponseError> { + return Odoo::handle_gotos(session, params, false) + } + + pub fn handle_goto_declaration(session: &mut SessionInfo, params: GotoDefinitionParams) -> Result, ResponseError> { + return Odoo::handle_gotos(session, params, true) + } + + fn handle_gotos(session: &mut SessionInfo, params: GotoDefinitionParams, is_declaration: bool) -> Result, ResponseError> { if session.sync_odoo.state_init == InitState::NOT_READY { return Ok(None); } - session.log_message(MessageType::INFO, format!("GoToDefinition requested on {} at {} - {}", + session.log_message(MessageType::INFO, format!("GoToDeclaration requested on {} at {} - {}", params.text_document_position_params.text_document.uri.to_string(), params.text_document_position_params.position.line, params.text_document_position_params.position.character)); @@ -1472,14 +1481,28 @@ impl Odoo { match ast_type { AstType::Python => { if file_info.borrow().file_info_ast.borrow().indexed_module.is_some() { - return Ok(DefinitionFeature::get_location(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); + if is_declaration { + return Ok(DeclarationFeature::get_location(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); + + } else { + return Ok(DefinitionFeature::get_location(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); + } } }, AstType::Xml => { - return Ok(DefinitionFeature::get_location_xml(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); + if is_declaration { + return Ok(DeclarationFeature::get_location_xml(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); + + } else { + return Ok(DefinitionFeature::get_location_xml(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); + } }, AstType::Csv => { - return Ok(DefinitionFeature::get_location_csv(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); + if is_declaration { + return Ok(DeclarationFeature::get_location_csv(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); + } else { + return Ok(DefinitionFeature::get_location_csv(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); + } }, } } diff --git a/server/src/features/declaration.rs b/server/src/features/declaration.rs new file mode 100644 index 00000000..5f4086d3 --- /dev/null +++ b/server/src/features/declaration.rs @@ -0,0 +1,122 @@ +use lsp_types::request::GotoDeclarationResponse; +use lsp_types::{LocationLink, Range}; +use std::path::PathBuf; +use std::{cell::RefCell, rc::Rc}; + +use crate::constants::SymType; +use crate::core::file_mgr::{FileInfo, FileMgr}; +use crate::core::symbols::symbol::Symbol; +use crate::features::goto_utils::{GotoRequest, GotoUtils}; +use crate::features::xml_ast_utils::{XmlAstResult, XmlAstUtils}; +use crate::threads::SessionInfo; +use crate::utils::PathSanitizer as _; + +pub struct DeclarationFeature {} + +impl DeclarationFeature { + + pub fn get_location(session: &mut SessionInfo, + file_symbol: &Rc>, + file_info: &Rc>, + line: u32, + character: u32 + ) -> Option { + let definitions_sources = GotoUtils::get_symbols(session, GotoRequest::Declaration, file_symbol, file_info, line, character); + let mut links = vec![]; + for def in definitions_sources.iter() { + if let Some(link) = GotoUtils::goto_source_to_location(session, def) { + links.push(link); + } + } + Some(GotoDeclarationResponse::Link(links)) + } + + pub fn get_location_xml(session: &mut SessionInfo, + file_symbol: &Rc>, + file_info: &Rc>, + line: u32, + character: u32 + ) -> Option { + let offset = file_info.borrow().position_to_offset(line, character, session.sync_odoo.encoding); + let data = file_info.borrow().file_info_ast.borrow().text_document.as_ref().unwrap().contents().to_string(); + let document = roxmltree::Document::parse(&data); + if let Ok(document) = document { + let root = document.root_element(); + let (symbols, link_range) = XmlAstUtils::get_symbols(session, file_symbol, root, offset, true); + if symbols.is_empty() { + return None; + } + let mut links = vec![]; + for xml_result in symbols.iter() { + match xml_result { + crate::features::xml_ast_utils::XmlAstResult::SYMBOL(s) => { + if let Some(file) = s.borrow().get_file() { + for path in file.upgrade().unwrap().borrow().paths().iter() { + let full_path = match file.upgrade().unwrap().borrow().typ() { + SymType::PACKAGE(_) => PathBuf::from(path).join(format!("__init__.py{}", file.upgrade().unwrap().borrow().as_package().i_ext())).sanitize(), + _ => path.clone() + }; + let range = if s.borrow().has_range() { + session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, &full_path, &s.borrow().range()) + } else { + Range::default() + }; + let link_range = if link_range.is_some() { + Some(session.sync_odoo.get_file_mgr().borrow().std_range_to_range(session, file_symbol.borrow().paths().first().as_ref().unwrap(), link_range.as_ref().unwrap())) + } else { + None + }; + links.push(LocationLink{ + origin_selection_range: link_range, + target_uri: FileMgr::pathname2uri(&full_path), + target_range: range, + target_selection_range: range + }); + } + } + }, + XmlAstResult::XML_DATA(xml_file_symbol, range) => { + let file = xml_file_symbol.borrow().get_file(); //in case of XML_DATA coming from a python class + if let Some(file) = file { + if let Some(file) = file.upgrade() { + for path in file.borrow().paths().iter() { + let full_path = match file.borrow().typ() { + SymType::PACKAGE(_) => PathBuf::from(path).join(format!("__init__.py{}", file.borrow().as_package().i_ext())).sanitize(), + _ => path.clone() + }; + let range = match file.borrow().typ() { + SymType::PACKAGE(_) | SymType::FILE | SymType::NAMESPACE | SymType::DISK_DIR => Range::default(), + _ => session.sync_odoo.get_file_mgr().borrow().std_range_to_range(session, &full_path, &range), + }; + let link_range = if link_range.is_some() { + Some(session.sync_odoo.get_file_mgr().borrow().std_range_to_range(session, file_symbol.borrow().paths().first().as_ref().unwrap(), link_range.as_ref().unwrap())) + } else { + None + }; + links.push(LocationLink{ + origin_selection_range: link_range, + target_uri: FileMgr::pathname2uri(&full_path), + target_range: range, + target_selection_range: range + }); + } + } + } + } + } + } + return Some(GotoDeclarationResponse::Link(links)); + } + None + } + + pub fn get_location_csv(_session: &mut SessionInfo, + _file_symbol: &Rc>, + _file_info: &Rc>, + _line: u32, + _character: u32 + ) -> Option { + None + } + +} diff --git a/server/src/features/definition.rs b/server/src/features/definition.rs index 30bf7718..3c6d8eb5 100644 --- a/server/src/features/definition.rs +++ b/server/src/features/definition.rs @@ -1,336 +1,29 @@ use lsp_types::{GotoDefinitionResponse, LocationLink, Range}; -use ruff_python_ast::{Expr, ExprCall}; -use ruff_text_size::TextSize; use std::path::PathBuf; use std::{cell::RefCell, rc::Rc}; -use crate::constants::{PackageType, SymType}; -use crate::core::evaluation::{Evaluation, EvaluationValue, ExprOrIdent}; +use crate::constants::SymType; use crate::core::file_mgr::{FileInfo, FileMgr}; -use crate::core::odoo::SyncOdoo; -use crate::core::python_odoo_builder::MAGIC_FIELDS; use crate::core::symbols::symbol::Symbol; -use crate::core::xml_data::OdooData; -use crate::features::ast_utils::AstUtils; -use crate::features::features_utils::FeaturesUtils; +use crate::features::goto_utils::{GotoRequest, GotoUtils}; use crate::features::xml_ast_utils::{XmlAstResult, XmlAstUtils}; -use crate::{S, oyarn}; use crate::threads::SessionInfo; use crate::utils::PathSanitizer as _; -pub enum DefinitionSourceType { - Symbol(Rc>), - OdooData(OdooData), - Module(Rc>), -} - -pub struct DefinitionSource { - pub source: DefinitionSourceType, - pub origin_selection_range: Option, -} - pub struct DefinitionFeature {} impl DefinitionFeature { - fn check_for_domain_field(session: &mut SessionInfo, eval: &Evaluation, file_symbol: &Rc>, call_expr: &Option, offset: usize, sources: &mut Vec) -> bool { - let (field_name, field_range) = if let Some(eval_value) = eval.value.as_ref() { - if let EvaluationValue::CONSTANT(Expr::StringLiteral(expr)) = eval_value { - (expr.value.to_string(), expr.range) - } else { - return false; - } - } else { - return false; - }; - let Some(call_expr) = call_expr else { return false }; - let module = file_symbol.borrow().find_module(); - let string_domain_fields = FeaturesUtils::find_argument_symbols( - session, Symbol::get_scope_symbol(file_symbol.clone(), offset as u32, false), module, &field_name, call_expr, offset, field_range - ); - let mut domain_found = false; - string_domain_fields.iter().for_each(|(field, field_range)|{ - if let Some(_) = field.borrow().get_file().and_then(|file_sym_weak| file_sym_weak.upgrade()){ - domain_found = true; - sources.push(DefinitionSource { - source: - DefinitionSourceType::Symbol(field.clone()), - origin_selection_range: Some(session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, file_symbol.borrow().paths().first().as_ref().unwrap(), &field_range)) - }); - } - }); - domain_found - } - - fn check_for_model_string(session: &mut SessionInfo, eval: &Evaluation, file_symbol: &Rc>, sources: &mut Vec) -> bool { - let value = if let Some(eval_value) = eval.value.as_ref() { - if let EvaluationValue::CONSTANT(Expr::StringLiteral(expr)) = eval_value { - oyarn!("{}", expr.value.to_string()) - } else { - return false; - } - } else { - return false; - }; - let model = session.sync_odoo.models.get(&value).cloned(); - let Some(model) = model else { - return false; - }; - let mut model_found = false; - let from_module = file_symbol.borrow().find_module(); - let classes = model.borrow().get_symbols(session, from_module.clone()); - let len_classes = classes.len(); - for class_symbol_rc in classes { - let class_symbol = class_symbol_rc.borrow(); - if let (Some(eval_range), Some(class_file)) = (eval.range, class_symbol.get_file().and_then(|file_sym_weak| file_sym_weak.upgrade())) { - if Rc::ptr_eq(file_symbol, &class_file) && class_symbol.range().contains(eval_range.start()) && len_classes > 1{ - continue; // if we are already on the class, skip, unless it is the only result - } - } - model_found = true; - sources.push(DefinitionSource { - source: DefinitionSourceType::Symbol(class_symbol_rc.clone()), - origin_selection_range: eval.range.map(|r| session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, file_symbol.borrow().paths().first().as_ref().unwrap(), &r)) - }); - } - model_found - } - - fn check_for_module_string(session: &mut SessionInfo, eval: &Evaluation, file_symbol: &Rc>, file_path: &String, sources: &mut Vec) -> bool { - if file_symbol.borrow().typ() != SymType::PACKAGE(PackageType::MODULE) || !file_path.ends_with("__manifest__.py") { - // If not on manifest, we don't check for modules - return false; - }; - let value = if let Some(eval_value) = eval.value.as_ref() { - if let EvaluationValue::CONSTANT(Expr::StringLiteral(expr)) = eval_value { - oyarn!("{}", expr.value.to_string()) - } else { - return false; - } - } else { - return false; - }; - let Some(module) = session.sync_odoo.modules.get(&oyarn!("{}", value)).and_then(|m| m.upgrade()) else { - return false; - }; - sources.push(DefinitionSource{ - source: DefinitionSourceType::Module(module.clone()), - origin_selection_range: None, - }); - true - } - - fn check_for_xml_id_string(session: &mut SessionInfo, eval: &Evaluation, file_symbol: &Rc>, sources: &mut Vec) -> bool { - let value = if let Some(eval_value) = eval.value.as_ref() { - if let EvaluationValue::CONSTANT(Expr::StringLiteral(expr)) = eval_value { - oyarn!("{}", expr.value.to_string()) - } else { - return false; - } - } else { - return false; - }; - let mut xml_found = false; - let xml_ids = SyncOdoo::get_xml_ids(session, file_symbol, value.as_str(), &std::ops::Range{start: 0, end: 0}, &mut vec![]); - for xml_id in xml_ids { - let file = xml_id.get_file_symbol(); - if let Some(file) = file { - if let Some(file) = file.upgrade() { - xml_found = true; - sources.push(DefinitionSource{ - source: DefinitionSourceType::OdooData(xml_id.clone()), - origin_selection_range: eval.range.map(|r| session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, file_symbol.borrow().paths().first().as_ref().unwrap(), &r)) - }); - } - } - } - xml_found - } - - fn check_for_compute_string(session: &mut SessionInfo, eval: &Evaluation, file_symbol: &Rc>, call_expr: &Option, offset: usize, sources: &mut Vec) -> bool { - let value = if let Some(eval_value) = eval.value.as_ref() { - if let EvaluationValue::CONSTANT(Expr::StringLiteral(expr)) = eval_value { - expr.value.to_string() - } else { - return false; - } - } else { - return false; - }; - let Some(call_expr) = call_expr else { return false }; - let method_symbols = FeaturesUtils::find_kwarg_methods_symbols( - session, Symbol::get_scope_symbol(file_symbol.clone(), offset as u32, false), file_symbol.borrow().find_module(), &value, call_expr, &offset - ); - let mut method_found = false; - method_symbols.iter().for_each(|field|{ - if let Some(file_sym) = field.borrow().get_file().and_then(|file_sym_weak| file_sym_weak.upgrade()){ - method_found = true; - sources.push(DefinitionSource { - source: DefinitionSourceType::Symbol(field.clone()), - origin_selection_range: eval.range.map(|r| session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, file_symbol.borrow().paths().first().as_ref().unwrap(), &r)) - }); - } - }); - method_found - } - - pub fn add_display_name_compute_methods(session: &mut SessionInfo, sources: &mut Vec, expr: &ExprOrIdent, file_symbol: &Rc>, offset: usize) { - // now we want `_compute_display_name` definition(s) - // we need the symbol of the model/ then we run get member symbol - // to do that, we need the expr, match it to attribute, get the value, get its evals - // with those evals, we run get_member_symbol on `_compute_display_name` - let crate::core::evaluation::ExprOrIdent::Expr(Expr::Attribute(attr_expr)) = expr else { - return; - }; - let (analyse_ast_result, _range) = AstUtils::get_symbol_from_expr(session, file_symbol, &crate::core::evaluation::ExprOrIdent::Expr(&attr_expr.value), offset as u32); - let eval_ptrs = analyse_ast_result.evaluations.iter().flat_map(|eval| Symbol::follow_ref(eval.symbol.get_symbol_ptr(), session, &mut None, false, false, None, None)).collect::>(); - let maybe_module = file_symbol.borrow().find_module(); - let symbols = eval_ptrs.iter().flat_map(|eval_ptr| { - let Some(symbol) = eval_ptr.upgrade_weak() else { - return vec![]; - }; - symbol.borrow().get_member_symbol(session, &S!("_compute_display_name"), maybe_module.clone(), false, false, true, true, false).0 - }).collect::>(); - for symbol in symbols { - if let Some(file) = symbol.borrow().get_file() { - for path in file.upgrade().unwrap().borrow().paths().iter() { - let full_path = match file.upgrade().unwrap().borrow().typ() { - SymType::PACKAGE(_) => PathBuf::from(path).join(format!("__init__.py{}", file.upgrade().unwrap().borrow().as_package().i_ext())).sanitize(), - _ => path.clone() - }; - let range = if symbol.borrow().has_range() { - if symbol.borrow().range().contains(TextSize::new(offset as u32)) { - continue; //skip if we are already on the definition - } - session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, &full_path, &symbol.borrow().range()) - } else { - Range::default() - }; - sources.push(DefinitionSource { - source: DefinitionSourceType::Symbol(symbol.clone()), - origin_selection_range: None, - }); - } - } - } - } - - pub fn get_symbols(session: &mut SessionInfo, - file_symbol: &Rc>, - file_info: &Rc>, - line: u32, - character: u32 - ) -> Vec { - let offset = file_info.borrow().position_to_offset(line, character, session.sync_odoo.encoding); - let file_info_ast_clone = file_info.borrow().file_info_ast.clone(); - let file_info_ast_ref = file_info_ast_clone.borrow(); - let (analyse_ast_result, _range, expr, call_expr) = AstUtils::get_symbols(session, &file_info_ast_ref, file_symbol, offset as u32); - if analyse_ast_result.evaluations.is_empty() { - return vec![]; - } - let mut definition_sources = vec![]; - let mut evaluations = analyse_ast_result.evaluations.clone(); - // Filter out magic fields - let mut dislay_name_found = false; - evaluations.retain(|eval| { - // Filter out, variables, whose parents are a class, whose name is one of the magic fields, and have the same range as their parent - let eval_sym = eval.symbol.get_symbol(session, &mut None, &mut vec![], None); - let Some(eval_sym) = eval_sym.upgrade_weak() else { return true; }; - if !MAGIC_FIELDS.contains(&eval_sym.borrow().name().as_str()) || eval_sym.borrow().typ() != SymType::VARIABLE || !eval_sym.borrow().is_field(session) { - return true; - } - if eval_sym.borrow().name() == "display_name" { - dislay_name_found = true; - } - let Some(parent_sym) = eval_sym.borrow().parent().and_then(|parent| parent.upgrade()) else { return true; }; - if parent_sym.borrow().typ() != SymType::CLASS { - return true; - } - eval_sym.borrow().range() != parent_sym.borrow().range() - }); - if let Some(expr) = expr && dislay_name_found { - DefinitionFeature::add_display_name_compute_methods(session, &mut definition_sources, &expr, file_symbol, offset); - } - drop(file_info_ast_ref); - let mut index = 0; - while index < evaluations.len() { - let eval = evaluations[index].clone(); - if DefinitionFeature::check_for_domain_field(session, &eval, file_symbol, &call_expr, offset, &mut definition_sources) || - DefinitionFeature::check_for_compute_string(session, &eval, file_symbol,&call_expr, offset, &mut definition_sources) || - DefinitionFeature::check_for_module_string(session, &eval, file_symbol, &file_info.borrow().uri, &mut definition_sources) || - DefinitionFeature::check_for_model_string(session, &eval, file_symbol, &mut definition_sources) || - DefinitionFeature::check_for_xml_id_string(session, &eval, file_symbol, &mut definition_sources) { - index += 1; - continue; - } - if matches!(eval.value, Some(EvaluationValue::CONSTANT(_))) { - // Skip go to definition on literals - index += 1; - continue; - } - let Some(symbol) = eval.symbol.get_symbol_as_weak(session, &mut None, &mut vec![], None).weak.upgrade() else { - index += 1; - continue; - }; - definition_sources.push(DefinitionSource{ - source: DefinitionSourceType::Symbol(symbol.clone()), - origin_selection_range: None - }); - index += 1; - } - definition_sources - } - - pub fn definition_source_to_location(session: &mut SessionInfo, def: &DefinitionSource) -> Option { - match &def.source { - DefinitionSourceType::Symbol(symbol) => { - if let Some(file_symbol) = symbol.borrow().get_file().and_then(|file_sym_weak| file_sym_weak.upgrade()){ - let path = file_symbol.borrow().get_symbol_first_path(); - let range = session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, &path, &symbol.borrow().range()); - return Some(LocationLink{ - origin_selection_range: def.origin_selection_range.clone(), - target_uri: FileMgr::pathname2uri(&path), - target_selection_range: range, - target_range: range, - }); - } - }, - DefinitionSourceType::Module(module) => { - let path = PathBuf::from(module.borrow().paths()[0].clone()).join("__manifest__.py").sanitize(); - return Some(LocationLink{ - origin_selection_range: None, - target_uri: FileMgr::pathname2uri(&path), - target_selection_range: Range::default(), - target_range: Range::default(), - }); - }, - DefinitionSourceType::OdooData(xml_id) => { - let file = xml_id.get_file_symbol(); - if let Some(file) = file { - if let Some(file) = file.upgrade() { - let range = session.sync_odoo.get_file_mgr().borrow().std_range_to_range(session, &file.borrow().paths()[0], &xml_id.get_range()); - return Some(LocationLink { - origin_selection_range: def.origin_selection_range.clone(), - target_uri: FileMgr::pathname2uri(&file.borrow().paths()[0]), - target_range: range, - target_selection_range: range }); - } - } - } - } - None - } - pub fn get_location(session: &mut SessionInfo, file_symbol: &Rc>, file_info: &Rc>, line: u32, character: u32 ) -> Option { - let definitions_sources = DefinitionFeature::get_symbols(session, file_symbol, file_info, line, character); + let definitions_sources = GotoUtils::get_symbols(session, GotoRequest::Definition, file_symbol, file_info, line, character); let mut links = vec![]; for def in definitions_sources.iter() { - if let Some(link) = DefinitionFeature::definition_source_to_location(session, def) { + if let Some(link) = GotoUtils::goto_source_to_location(session, def) { links.push(link); } } diff --git a/server/src/features/goto_utils.rs b/server/src/features/goto_utils.rs new file mode 100644 index 00000000..5a9095df --- /dev/null +++ b/server/src/features/goto_utils.rs @@ -0,0 +1,319 @@ +use std::{cell::RefCell, path::PathBuf, rc::Rc}; + +use lsp_types::{LocationLink, Range}; +use ruff_python_ast::{Expr, ExprCall}; + +use crate::{S, constants::{PackageType, SymType}, core::{evaluation::{Evaluation, EvaluationValue, ExprOrIdent}, file_mgr::{FileInfo, FileMgr}, odoo::SyncOdoo, python_odoo_builder::MAGIC_FIELDS, symbols::symbol::Symbol, xml_data::OdooData}, features::{ast_utils::AstUtils, features_utils::FeaturesUtils}, oyarn, threads::SessionInfo, utils::PathSanitizer}; + +pub enum GotoRequest { + Definition, + Declaration, +} + +pub enum GotoSourceType { + Symbol(Rc>), + OdooData(OdooData), + Module(Rc>), +} + +pub struct GotoSource { + pub source: GotoSourceType, + pub origin_selection_range: Option, +} +pub struct GotoUtils {} + +impl GotoUtils { + fn check_for_domain_field(session: &mut SessionInfo, eval: &Evaluation, file_symbol: &Rc>, call_expr: &Option, offset: usize, sources: &mut Vec) -> bool { + let (field_name, field_range) = if let Some(eval_value) = eval.value.as_ref() { + if let EvaluationValue::CONSTANT(Expr::StringLiteral(expr)) = eval_value { + (expr.value.to_string(), expr.range) + } else { + return false; + } + } else { + return false; + }; + let Some(call_expr) = call_expr else { return false }; + let module = file_symbol.borrow().find_module(); + let string_domain_fields = FeaturesUtils::find_argument_symbols( + session, Symbol::get_scope_symbol(file_symbol.clone(), offset as u32, false), module, &field_name, call_expr, offset, field_range + ); + let mut domain_found = false; + string_domain_fields.iter().for_each(|(field, field_range)|{ + if let Some(_) = field.borrow().get_file().and_then(|file_sym_weak| file_sym_weak.upgrade()){ + domain_found = true; + sources.push(GotoSource { + source: + GotoSourceType::Symbol(field.clone()), + origin_selection_range: Some(session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, file_symbol.borrow().paths().first().as_ref().unwrap(), &field_range)) + }); + } + }); + domain_found + } + + fn check_for_model_string(session: &mut SessionInfo, eval: &Evaluation, file_symbol: &Rc>, sources: &mut Vec) -> bool { + let value = if let Some(eval_value) = eval.value.as_ref() { + if let EvaluationValue::CONSTANT(Expr::StringLiteral(expr)) = eval_value { + oyarn!("{}", expr.value.to_string()) + } else { + return false; + } + } else { + return false; + }; + let model = session.sync_odoo.models.get(&value).cloned(); + let Some(model) = model else { + return false; + }; + let mut model_found = false; + let from_module = file_symbol.borrow().find_module(); + let classes = model.borrow().get_symbols(session, from_module.clone()); + let len_classes = classes.len(); + for class_symbol_rc in classes { + let class_symbol = class_symbol_rc.borrow(); + if let (Some(eval_range), Some(class_file)) = (eval.range, class_symbol.get_file().and_then(|file_sym_weak| file_sym_weak.upgrade())) { + if Rc::ptr_eq(file_symbol, &class_file) && class_symbol.range().contains(eval_range.start()) && len_classes > 1{ + continue; // if we are already on the class, skip, unless it is the only result + } + } + model_found = true; + sources.push(GotoSource { + source: GotoSourceType::Symbol(class_symbol_rc.clone()), + origin_selection_range: eval.range.map(|r| session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, file_symbol.borrow().paths().first().as_ref().unwrap(), &r)) + }); + } + model_found + } + + fn check_for_module_string(session: &mut SessionInfo, eval: &Evaluation, file_symbol: &Rc>, file_path: &String, sources: &mut Vec) -> bool { + if file_symbol.borrow().typ() != SymType::PACKAGE(PackageType::MODULE) || !file_path.ends_with("__manifest__.py") { + // If not on manifest, we don't check for modules + return false; + }; + let value = if let Some(eval_value) = eval.value.as_ref() { + if let EvaluationValue::CONSTANT(Expr::StringLiteral(expr)) = eval_value { + oyarn!("{}", expr.value.to_string()) + } else { + return false; + } + } else { + return false; + }; + let Some(module) = session.sync_odoo.modules.get(&oyarn!("{}", value)).and_then(|m| m.upgrade()) else { + return false; + }; + sources.push(GotoSource{ + source: GotoSourceType::Module(module.clone()), + origin_selection_range: None, + }); + true + } + + fn check_for_xml_id_string(session: &mut SessionInfo, eval: &Evaluation, file_symbol: &Rc>, sources: &mut Vec) -> bool { + let value = if let Some(eval_value) = eval.value.as_ref() { + if let EvaluationValue::CONSTANT(Expr::StringLiteral(expr)) = eval_value { + oyarn!("{}", expr.value.to_string()) + } else { + return false; + } + } else { + return false; + }; + let mut xml_found = false; + let xml_ids = SyncOdoo::get_xml_ids(session, file_symbol, value.as_str(), &std::ops::Range{start: 0, end: 0}, &mut vec![]); + for xml_id in xml_ids { + let file = xml_id.get_file_symbol(); + if let Some(file) = file { + if let Some(file) = file.upgrade() { + xml_found = true; + sources.push(GotoSource{ + source: GotoSourceType::OdooData(xml_id.clone()), + origin_selection_range: eval.range.map(|r| session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, file_symbol.borrow().paths().first().as_ref().unwrap(), &r)) + }); + } + } + } + xml_found + } + + fn check_for_compute_string(session: &mut SessionInfo, eval: &Evaluation, file_symbol: &Rc>, call_expr: &Option, offset: usize, sources: &mut Vec) -> bool { + let value = if let Some(eval_value) = eval.value.as_ref() { + if let EvaluationValue::CONSTANT(Expr::StringLiteral(expr)) = eval_value { + expr.value.to_string() + } else { + return false; + } + } else { + return false; + }; + let Some(call_expr) = call_expr else { return false }; + let method_symbols = FeaturesUtils::find_kwarg_methods_symbols( + session, Symbol::get_scope_symbol(file_symbol.clone(), offset as u32, false), file_symbol.borrow().find_module(), &value, call_expr, &offset + ); + let mut method_found = false; + method_symbols.iter().for_each(|field|{ + if let Some(file_sym) = field.borrow().get_file().and_then(|file_sym_weak| file_sym_weak.upgrade()){ + method_found = true; + sources.push(GotoSource { + source: GotoSourceType::Symbol(field.clone()), + origin_selection_range: eval.range.map(|r| session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, file_sym.borrow().paths().first().as_ref().unwrap(), &r)) + }); + } + }); + method_found + } + + pub fn add_display_name_compute_methods(session: &mut SessionInfo, sources: &mut Vec, expr: &ExprOrIdent, file_symbol: &Rc>, offset: usize) { + // now we want `_compute_display_name` definition(s) + // we need the symbol of the model/ then we run get member symbol + // to do that, we need the expr, match it to attribute, get the value, get its evals + // with those evals, we run get_member_symbol on `_compute_display_name` + let crate::core::evaluation::ExprOrIdent::Expr(Expr::Attribute(attr_expr)) = expr else { + return; + }; + let (analyse_ast_result, _range) = AstUtils::get_symbol_from_expr(session, file_symbol, &crate::core::evaluation::ExprOrIdent::Expr(&attr_expr.value), offset as u32); + let eval_ptrs = analyse_ast_result.evaluations.iter().flat_map(|eval| Symbol::follow_ref(eval.symbol.get_symbol_ptr(), session, &mut None, false, false, None, None)).collect::>(); + let maybe_module = file_symbol.borrow().find_module(); + let symbols = eval_ptrs.iter().flat_map(|eval_ptr| { + let Some(symbol) = eval_ptr.upgrade_weak() else { + return vec![]; + }; + symbol.borrow().get_member_symbol(session, &S!("_compute_display_name"), maybe_module.clone(), false, false, true, true, false).0 + }).collect::>(); + for symbol in symbols { + if let Some(file) = symbol.borrow().get_file() { + for path in file.upgrade().unwrap().borrow().paths().iter() { + sources.push(GotoSource { + source: GotoSourceType::Symbol(symbol.clone()), + origin_selection_range: None, + }); + } + } + } + } + + pub fn get_symbols(session: &mut SessionInfo, + goto_request: GotoRequest, + file_symbol: &Rc>, + file_info: &Rc>, + line: u32, + character: u32 + ) -> Vec { + let offset = file_info.borrow().position_to_offset(line, character, session.sync_odoo.encoding); + let file_info_ast_clone = file_info.borrow().file_info_ast.clone(); + let file_info_ast_ref = file_info_ast_clone.borrow(); + let (analyse_ast_result, _range, expr, call_expr) = AstUtils::get_symbols(session, &file_info_ast_ref, file_symbol, offset as u32); + if analyse_ast_result.evaluations.is_empty() { + return vec![]; + } + let mut definition_sources = vec![]; + let mut evaluations = analyse_ast_result.evaluations.clone(); + // Filter out magic fields + let mut dislay_name_found = false; + evaluations.retain(|eval| { + // Filter out, variables, whose parents are a class, whose name is one of the magic fields, and have the same range as their parent + let eval_sym = eval.symbol.get_symbol(session, &mut None, &mut vec![], None); + let Some(eval_sym) = eval_sym.upgrade_weak() else { return true; }; + if !MAGIC_FIELDS.contains(&eval_sym.borrow().name().as_str()) || eval_sym.borrow().typ() != SymType::VARIABLE || !eval_sym.borrow().is_field(session) { + return true; + } + if eval_sym.borrow().name() == "display_name" { + dislay_name_found = true; + } + let Some(parent_sym) = eval_sym.borrow().parent().and_then(|parent| parent.upgrade()) else { return true; }; + if parent_sym.borrow().typ() != SymType::CLASS { + return true; + } + eval_sym.borrow().range() != parent_sym.borrow().range() + }); + if let Some(expr) = expr && dislay_name_found { + GotoUtils::add_display_name_compute_methods(session, &mut definition_sources, &expr, file_symbol, offset); + } + drop(file_info_ast_ref); + let mut index = 0; + while index < evaluations.len() { + let eval = evaluations[index].clone(); + if GotoUtils::check_for_domain_field(session, &eval, file_symbol, &call_expr, offset, &mut definition_sources) || + GotoUtils::check_for_compute_string(session, &eval, file_symbol,&call_expr, offset, &mut definition_sources) || + GotoUtils::check_for_module_string(session, &eval, file_symbol, &file_info.borrow().uri, &mut definition_sources) || + GotoUtils::check_for_model_string(session, &eval, file_symbol, &mut definition_sources) || + GotoUtils::check_for_xml_id_string(session, &eval, file_symbol, &mut definition_sources) { + index += 1; + continue; + } + if matches!(eval.value, Some(EvaluationValue::CONSTANT(_))) { + // Skip go to definition on literals + index += 1; + continue; + } + if matches!(goto_request, GotoRequest::Definition) { + let eval_ptr = eval.symbol.get_symbol(session, &mut None, &mut vec![], None); + let end_symbols = Symbol::follow_ref(&eval_ptr, session, &mut None, true, false, None, None); + for end_symbol in end_symbols.iter() { + if let Some(symbol) = end_symbol.upgrade_weak() { + definition_sources.push(GotoSource{ + source: GotoSourceType::Symbol(symbol.clone()), + origin_selection_range: None + }); + } + } + } else { + let Some(symbol) = eval.symbol.get_symbol_as_weak(session, &mut None, &mut vec![], None).weak.upgrade() else { + index += 1; + continue; + }; + definition_sources.push(GotoSource{ + source: GotoSourceType::Symbol(symbol.clone()), + origin_selection_range: None + }); + } + index += 1; + } + definition_sources + } + + pub fn goto_source_to_location(session: &mut SessionInfo, def: &GotoSource) -> Option { + match &def.source { + GotoSourceType::Symbol(symbol) => { + if let Some(file_symbol) = symbol.borrow().get_file().and_then(|file_sym_weak| file_sym_weak.upgrade()){ + let path = file_symbol.borrow().get_symbol_first_path(); + let symbol_range = if symbol.borrow().has_range() { + session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, &path, &symbol.borrow().range()) + } else { + Range::default() + }; + return Some(LocationLink{ + origin_selection_range: def.origin_selection_range.clone(), + target_uri: FileMgr::pathname2uri(&path), + target_selection_range: symbol_range, + target_range: symbol_range, + }); + } + }, + GotoSourceType::Module(module) => { + let path = PathBuf::from(module.borrow().paths()[0].clone()).join("__manifest__.py").sanitize(); + return Some(LocationLink{ + origin_selection_range: None, + target_uri: FileMgr::pathname2uri(&path), + target_selection_range: Range::default(), + target_range: Range::default(), + }); + }, + GotoSourceType::OdooData(xml_id) => { + let file = xml_id.get_file_symbol(); + if let Some(file) = file { + if let Some(file) = file.upgrade() { + let range = session.sync_odoo.get_file_mgr().borrow().std_range_to_range(session, &file.borrow().paths()[0], &xml_id.get_range()); + return Some(LocationLink { + origin_selection_range: def.origin_selection_range.clone(), + target_uri: FileMgr::pathname2uri(&file.borrow().paths()[0]), + target_range: range, + target_selection_range: range }); + } + } + } + } + None + } +} \ No newline at end of file diff --git a/server/src/features/mod.rs b/server/src/features/mod.rs index e64f22f5..0a252445 100644 --- a/server/src/features/mod.rs +++ b/server/src/features/mod.rs @@ -1,8 +1,10 @@ pub mod ast_utils; pub mod completion; +pub mod declaration; pub mod definition; pub mod document_symbols; pub mod features_utils; +pub mod goto_utils; pub mod hover; pub mod node_index_ast; pub mod references; diff --git a/server/src/features/references.rs b/server/src/features/references.rs index 30b066e0..7b406ae7 100644 --- a/server/src/features/references.rs +++ b/server/src/features/references.rs @@ -4,12 +4,13 @@ use lsp_types::{Location, Range}; use ruff_python_ast::visitor::{walk_expr, walk_stmt, Visitor}; use ruff_python_ast::{Alias, Expr, Identifier, Stmt, StmtAnnAssign, StmtAssert, StmtAssign, StmtAugAssign, StmtClassDef, StmtIf, StmtMatch, StmtRaise, StmtReturn, StmtTry, StmtTypeAlias, StmtWith}; use ruff_text_size::{Ranged, TextRange, TextSize}; +use tracing::error; use crate::S; use crate::constants::OYarn; use crate::core::evaluation::{Evaluation, EvaluationSymbolPtr}; use crate::core::odoo::SyncOdoo; -use crate::features::definition::{DefinitionFeature, DefinitionSourceType}; +use crate::features::goto_utils::{GotoRequest, GotoSourceType, GotoUtils}; use crate::{constants::SymType, core::{file_mgr::{FileInfo, FileMgr}, symbols::symbol::Symbol}, features::xml_ast_utils::{XmlAstResult, XmlAstUtils}, threads::SessionInfo, utils::PathSanitizer}; @@ -25,12 +26,12 @@ impl ReferenceFeature { /// TODO: Odoo specific (XML field refs, string-based model refs) pub fn get_references(session: &mut SessionInfo, file_symbol: &Rc>, file_info: &Rc>, line: u32, character: u32) -> Option> { //We want to search for references of the definition, and not the current symbol. Let's use definition feature for that - let def_sources = DefinitionFeature::get_symbols(session, file_symbol, file_info, line, character); + let def_sources = GotoUtils::get_symbols(session, GotoRequest::Definition, file_symbol, file_info, line, character); let mut locations = Vec::new(); for definition in def_sources.iter() { match &definition.source { - DefinitionSourceType::Symbol(target_symbol) => { + GotoSourceType::Symbol(target_symbol) => { let symbol_name = target_symbol.borrow().name().to_string(); locations.extend(ReferenceFeature::references_in_file(session, file_symbol, file_info, &symbol_name, &target_symbol)); @@ -80,6 +81,7 @@ impl ReferenceFeature { fn references_in_file(session: &mut SessionInfo, file_symbol: &Rc>, file_info: &Rc>, symbol_name: &String, target_symbol_rc: &Rc>) -> Vec { SyncOdoo::process_rebuilds(session, false); + error!("Searching references for symbol {} in file {}", symbol_name, file_symbol.borrow().paths()[0]); let file_info_ast = file_info.borrow().file_info_ast.clone(); let mut visitor = ReferenceVisitor { sym_stack: vec![], diff --git a/server/src/server.rs b/server/src/server.rs index 15a7cb04..efb12b4e 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -2,7 +2,7 @@ use std::{io::Error, panic, sync::{Arc, Mutex, atomic::AtomicBool}, thread::Join use crossbeam_channel::{Receiver, Select, Sender}; use lsp_server::{Connection, IoThreads, Message, ProtocolError, RequestId, ResponseError}; -use lsp_types::{CancelParams, CompletionOptions, DefinitionOptions, DocumentSymbolOptions, FileOperationFilter, FileOperationPattern, FileOperationRegistrationOptions, HoverProviderCapability, InitializeParams, InitializeResult, OneOf, ReferencesOptions, SaveOptions, ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, WorkDoneProgressOptions, WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities, WorkspaceSymbolOptions, notification::{Cancel, DidChangeConfiguration, DidChangeTextDocument, DidChangeWatchedFiles, DidChangeWorkspaceFolders, DidCloseTextDocument, DidCreateFiles, DidDeleteFiles, DidOpenTextDocument, DidRenameFiles, DidSaveTextDocument, Notification}, request::{Completion, DocumentSymbolRequest, GotoDefinition, HoverRequest, References, Request, ResolveCompletionItem, Shutdown, WorkspaceSymbolRequest, WorkspaceSymbolResolve}}; +use lsp_types::{CancelParams, CompletionOptions, DeclarationOptions, DefinitionOptions, DocumentSymbolOptions, FileOperationFilter, FileOperationPattern, FileOperationRegistrationOptions, HoverProviderCapability, InitializeParams, InitializeResult, OneOf, ReferencesOptions, SaveOptions, ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, WorkDoneProgressOptions, WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities, WorkspaceSymbolOptions, notification::{Cancel, DidChangeConfiguration, DidChangeTextDocument, DidChangeWatchedFiles, DidChangeWorkspaceFolders, DidCloseTextDocument, DidCreateFiles, DidDeleteFiles, DidOpenTextDocument, DidRenameFiles, DidSaveTextDocument, Notification}, request::{Completion, DocumentSymbolRequest, GotoDeclaration, GotoDefinition, HoverRequest, References, Request, ResolveCompletionItem, Shutdown, WorkspaceSymbolRequest, WorkspaceSymbolResolve}}; use ruff_source_file::PositionEncoding; use serde_json::json; #[cfg(target_os = "linux")] @@ -159,6 +159,13 @@ impl Server { save: Some(lsp_types::TextDocumentSyncSaveOptions::SaveOptions(SaveOptions{include_text: Some(false)})) //TODO could deactivate if set on 'afterDelay? })), hover_provider: Some(HoverProviderCapability::Simple(true)), + declaration_provider: Some(lsp_types::DeclarationCapability::Options( + DeclarationOptions { + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: Some(false) + } + } + )), definition_provider: Some(OneOf::Right(DefinitionOptions{ work_done_progress_options: WorkDoneProgressOptions{ work_done_progress: Some(false) @@ -382,7 +389,7 @@ impl Server { Message::Request(r) => { self.running_request_ids.lock().unwrap().push(r.id.clone()); match r.method.as_str() { - HoverRequest::METHOD | GotoDefinition::METHOD | References::METHOD | DocumentSymbolRequest::METHOD | + HoverRequest::METHOD | GotoDefinition::METHOD | GotoDeclaration::METHOD | References::METHOD | DocumentSymbolRequest::METHOD | WorkspaceSymbolRequest::METHOD | WorkspaceSymbolResolve::METHOD | Completion::METHOD => { self.interrupt_rebuild_boolean.store(true, std::sync::atomic::Ordering::SeqCst); if DEBUG_THREADS { diff --git a/server/src/threads.rs b/server/src/threads.rs index e20afd08..2aa62308 100644 --- a/server/src/threads.rs +++ b/server/src/threads.rs @@ -2,9 +2,9 @@ use std::{collections::VecDeque, path::PathBuf, sync::{Arc, Mutex}, time::Instan use crossbeam_channel::{Receiver, Sender, TryRecvError}; use lsp_server::{Message, RequestId, Response, ResponseError}; -use lsp_types::{CompletionResponse, DocumentSymbolResponse, Hover, Location, LogMessageParams, MessageType, ShowMessageParams, WorkspaceSymbol, WorkspaceSymbolResponse, notification::{DidChangeConfiguration, DidChangeTextDocument, DidChangeWatchedFiles, DidChangeWorkspaceFolders, +use lsp_types::{CompletionResponse, DocumentSymbolResponse, GotoDefinitionResponse, Hover, Location, LogMessageParams, MessageType, ShowMessageParams, WorkspaceSymbol, WorkspaceSymbolResponse, notification::{DidChangeConfiguration, DidChangeTextDocument, DidChangeWatchedFiles, DidChangeWorkspaceFolders, DidCloseTextDocument, DidCreateFiles, DidDeleteFiles, DidOpenTextDocument, DidRenameFiles, DidSaveTextDocument, LogMessage, - Notification, ShowMessage}, request::{Completion, DocumentSymbolRequest, GotoDefinition, GotoTypeDefinitionResponse, HoverRequest, References, Request, Shutdown, WorkspaceSymbolRequest, WorkspaceSymbolResolve}}; + Notification, ShowMessage}, request::{Completion, DocumentSymbolRequest, GotoDeclaration, GotoDeclarationResponse, GotoDefinition, GotoTypeDefinitionResponse, HoverRequest, References, Request, Shutdown, WorkspaceSymbolRequest, WorkspaceSymbolResolve}}; use serde::{de::DeserializeOwned, Serialize}; use serde_json::Value; use tracing::{error, info, warn}; @@ -354,7 +354,12 @@ pub fn message_processor_thread_main(sync_odoo: Arc>, generic_re GotoDefinition::METHOD => { let mut session = create_session!(sender, receiver, sync_odoo, delayed_process_sender); SyncOdoo::process_rebuilds(&mut session, true); - to_value::(Odoo::handle_goto_definition(&mut session, serde_json::from_value(r.params).unwrap())) + to_value::(Odoo::handle_goto_definition(&mut session, serde_json::from_value(r.params).unwrap())) + }, + GotoDeclaration::METHOD => { + let mut session = create_session!(sender, receiver, sync_odoo, delayed_process_sender); + SyncOdoo::process_rebuilds(&mut session, true); + to_value::(Odoo::handle_goto_declaration(&mut session, serde_json::from_value(r.params).unwrap())) }, References::METHOD => { let mut session = create_session!(sender, receiver, sync_odoo, delayed_process_sender); From 0475a75a500f1672f5e903d6fd425ccc3cdb21d9 Mon Sep 17 00:00:00 2001 From: fda-odoo Date: Thu, 19 Feb 2026 13:00:57 +0100 Subject: [PATCH 4/9] [IMP] no_hash hashmap + cache for typeshed types --- server/src/core/evaluation.rs | 41 ++++--- server/src/core/odoo.rs | 125 ++++++++++++++++++++- server/src/core/symbols/class_symbol.rs | 5 +- server/src/core/symbols/file_symbol.rs | 6 +- server/src/core/symbols/function_symbol.rs | 6 +- server/src/core/symbols/module_symbol.rs | 6 +- server/src/core/symbols/package_symbol.rs | 6 +- server/src/core/symbols/symbol.rs | 4 +- server/src/core/symbols/symbol_mgr.rs | 7 +- server/src/features/references.rs | 3 +- server/src/server.rs | 2 +- server/src/utils.rs | 24 ++++ 12 files changed, 190 insertions(+), 45 deletions(-) diff --git a/server/src/core/evaluation.rs b/server/src/core/evaluation.rs index 92994561..08b92daa 100644 --- a/server/src/core/evaluation.rs +++ b/server/src/core/evaluation.rs @@ -11,6 +11,7 @@ use std::i32; use std::rc::{Rc, Weak}; use std::cell::RefCell; use crate::core::diagnostics::{create_diagnostic, DiagnosticCode}; +use crate::utils::NoHashBuilder; use crate::{constants::*, Sy}; use crate::core::odoo::SyncOdoo; use crate::threads::SessionInfo; @@ -283,7 +284,7 @@ impl Evaluation { Evaluation { symbol: EvaluationSymbol { sym: EvaluationSymbolPtr::WEAK(EvaluationSymbolWeak{ - weak: Rc::downgrade(&odoo.get_symbol("", &(vec![Sy!("builtins")], vec![Sy!("list")]), u32::MAX).last().expect("builtins list not found")), + weak: odoo.get_ts_list(), context: HashMap::new(), instance: Some(true), is_super: false, @@ -299,7 +300,7 @@ impl Evaluation { Evaluation { symbol: EvaluationSymbol { sym: EvaluationSymbolPtr::WEAK(EvaluationSymbolWeak{ - weak: Rc::downgrade(&odoo.get_symbol("", &(vec![Sy!("builtins")], vec![Sy!("tuple")]), u32::MAX).last().expect("builtins list not found")), + weak: odoo.get_ts_tuple(), context: HashMap::new(), instance: Some(true), is_super: false, @@ -315,7 +316,7 @@ impl Evaluation { Evaluation { symbol: EvaluationSymbol { sym: EvaluationSymbolPtr::WEAK(EvaluationSymbolWeak{ - weak: Rc::downgrade(&odoo.get_symbol("", &(vec![Sy!("builtins")], vec![Sy!("dict")]), u32::MAX).last().expect("builtins list not found")), + weak: odoo.get_ts_dict(), context: HashMap::new(), instance: Some(true), is_super: false, @@ -331,7 +332,7 @@ impl Evaluation { Evaluation { symbol: EvaluationSymbol { sym: EvaluationSymbolPtr::WEAK(EvaluationSymbolWeak{ - weak: Rc::downgrade(&odoo.get_symbol("", &(vec![Sy!("builtins")], vec![Sy!("set")]), u32::MAX).last().expect("builtins set not found")), + weak: odoo.get_ts_set(), context: HashMap::new(), instance: Some(true), is_super: false, @@ -355,25 +356,25 @@ impl Evaluation { } pub fn new_constant(odoo: &mut SyncOdoo, values: Expr, range: TextRange) -> Evaluation { - let tree_value = match &values { + let symbol = match &values { Expr::StringLiteral(_s) => { - (vec![Sy!("builtins")], vec![Sy!("str")]) + odoo.get_ts_string() }, Expr::BooleanLiteral(_b) => { - (vec![Sy!("builtins")], vec![Sy!("bool")]) + odoo.get_ts_boolean() }, Expr::NumberLiteral(_n) => { match _n.value { - Number::Float(_) => (vec![Sy!("builtins")], vec![Sy!("float")]), - Number::Int(_) => (vec![Sy!("builtins")], vec![Sy!("int")]), - Number::Complex { .. } => (vec![Sy!("builtins")], vec![Sy!("complex")]), + Number::Float(_) => odoo.get_ts_float(), + Number::Int(_) => odoo.get_ts_int(), + Number::Complex { .. } => odoo.get_ts_complex(), } }, Expr::BytesLiteral(_b) => { - (vec![Sy!("builtins")], vec![Sy!("bytes")]) + odoo.get_ts_bytes() }, Expr::EllipsisLiteral(_e) => { - (vec![Sy!("builtins")], vec![Sy!("Ellipsis")]) + odoo.get_ts_ellipsis() }, Expr::NoneLiteral(_n) => { let mut eval = Evaluation::new_none(); @@ -381,14 +382,10 @@ impl Evaluation { eval.value = Some(EvaluationValue::CONSTANT(values)); return eval } - _ => {(vec![Sy!("builtins")], vec![Sy!("object")])} + _ => { + odoo.get_ts_object() + } }; - let symbol; - if !values.is_none_literal_expr() { - symbol = Rc::downgrade(&odoo.get_symbol("", &tree_value, u32::MAX).last().expect("builtins class not found")); - } else { - symbol = Weak::new(); - } Evaluation { symbol: EvaluationSymbol { sym: EvaluationSymbolPtr::WEAK(EvaluationSymbolWeak{ @@ -512,7 +509,7 @@ impl Evaluation { /// else: /// i="test" /// It will return two evaluation for i, one with 5 and one for "test" - pub fn from_sections(parent: &Symbol, sections: &HashMap>>>) -> Vec { + pub fn from_sections(parent: &Symbol, sections: &HashMap>>, NoHashBuilder>) -> Vec { let mut res = vec![]; let section = parent.as_symbol_mgr().get_section_for(u32::MAX); let content_symbols = parent.as_symbol_mgr()._get_loc_symbol(sections, u32::MAX, &SectionIndex::INDEX(section.index), &mut HashSet::new()); @@ -1299,7 +1296,7 @@ impl Evaluation { evals.push(Evaluation { symbol: EvaluationSymbol { sym: EvaluationSymbolPtr::WEAK(EvaluationSymbolWeak{ - weak: Rc::downgrade(&odoo.get_symbol("", &(vec![Sy!("builtins")], vec![Sy!("bool")]), u32::MAX).last().expect("builtins class not found")), + weak: odoo.get_ts_boolean(), context: HashMap::new(), instance: Some(true), is_super: false, @@ -1347,7 +1344,7 @@ impl Evaluation { Evaluation { symbol: EvaluationSymbol { sym: EvaluationSymbolPtr::WEAK(EvaluationSymbolWeak{ - weak: Rc::downgrade(&odoo.get_symbol("", &(vec![Sy!("builtins")], vec![Sy!("str")]), u32::MAX).last().expect("builtins class not found")), + weak: odoo.get_ts_string(), context: HashMap::new(), instance: Some(true), is_super: false, diff --git a/server/src/core/odoo.rs b/server/src/core/odoo.rs index 62ae86b3..e850280e 100644 --- a/server/src/core/odoo.rs +++ b/server/src/core/odoo.rs @@ -28,6 +28,7 @@ use request::{RegisterCapability, Request, WorkspaceConfiguration}; use ruff_source_file::PositionEncoding; use serde_json::Value; use tracing::{error, warn, info, trace}; +use weak_table::traits::WeakElement; use std::collections::HashSet; use std::process::Command; @@ -62,6 +63,42 @@ pub enum InitState { ODOO_READY, } +#[derive(Debug)] +pub struct TypeshedWeakReferences { + dict: Weak>, + tuple: Weak>, + set: Weak>, + list: Weak>, + string: Weak>, + boolean: Weak>, + int: Weak>, + float: Weak>, + complex: Weak>, + ellipsis: Weak>, + bytes: Weak>, + object: Weak>, +} + +impl TypeshedWeakReferences { + + pub fn new() -> Self { + Self { + dict: Weak::new(), + tuple: Weak::new(), + set: Weak::new(), + list: Weak::new(), + string: Weak::new(), + boolean: Weak::new(), + int: Weak::new(), + float: Weak::new(), + complex: Weak::new(), + ellipsis: Weak::new(), + bytes: Weak::new(), + object: Weak::new(), + } + } +} + #[derive(Debug)] pub struct SyncOdoo { pub version_major: u32, @@ -102,6 +139,8 @@ pub struct SyncOdoo { pub opened_files: Vec, pub evaluation_search: Option>>, //If set, any evaluation will be check against this value. If evaluation matches, location is kept in evaluation_locations pub evaluation_locations: Vec, + pub typeshed_weak_cache: TypeshedWeakReferences, //cache of weak references to important typeshed symbols, to avoid having to look for them in the graph for each evaluation + pub test_mode: bool, } @@ -150,7 +189,7 @@ impl SyncOdoo { opened_files: vec![], evaluation_search: None, evaluation_locations: vec![], - + typeshed_weak_cache: TypeshedWeakReferences::new(), test_mode: false, }; sync_odoo @@ -1209,6 +1248,90 @@ impl SyncOdoo { module.as_module_package().get_xml_id(&oyarn!("{}", id_split.last().unwrap())) } + pub fn get_ts_dict(&mut self) -> Weak> { + if self.typeshed_weak_cache.dict.is_expired() { + self.typeshed_weak_cache.dict = Rc::downgrade(&self.get_symbol("", &(vec![Sy!("builtins")], vec![Sy!("dict")]), u32::MAX).last().unwrap().clone()); + } + self.typeshed_weak_cache.dict.clone() + } + + pub fn get_ts_tuple(&mut self) -> Weak> { + if self.typeshed_weak_cache.tuple.is_expired() { + self.typeshed_weak_cache.tuple = Rc::downgrade(&self.get_symbol("", &(vec![Sy!("builtins")], vec![Sy!("tuple")]), u32::MAX).last().unwrap().clone()); + } + self.typeshed_weak_cache.tuple.clone() + } + + pub fn get_ts_set(&mut self) -> Weak> { + if self.typeshed_weak_cache.set.is_expired() { + self.typeshed_weak_cache.set = Rc::downgrade(&self.get_symbol("", &(vec![Sy!("builtins")], vec![Sy!("set")]), u32::MAX).last().unwrap().clone()); + } + self.typeshed_weak_cache.set.clone() + } + + pub fn get_ts_list(&mut self) -> Weak> { + if self.typeshed_weak_cache.list.is_expired() { + self.typeshed_weak_cache.list = Rc::downgrade(&self.get_symbol("", &(vec![Sy!("builtins")], vec![Sy!("list")]), u32::MAX).last().unwrap().clone()); + } + self.typeshed_weak_cache.list.clone() + } + + pub fn get_ts_string(&mut self) -> Weak> { + if self.typeshed_weak_cache.string.is_expired() { + self.typeshed_weak_cache.string = Rc::downgrade(&self.get_symbol("", &(vec![Sy!("builtins")], vec![Sy!("str")]), u32::MAX).last().unwrap().clone()); + } + self.typeshed_weak_cache.string.clone() + } + + pub fn get_ts_boolean(&mut self) -> Weak> { + if self.typeshed_weak_cache.boolean.is_expired() { + self.typeshed_weak_cache.boolean = Rc::downgrade(&self.get_symbol("", &(vec![Sy!("builtins")], vec![Sy!("bool")]), u32::MAX).last().unwrap().clone()); + } + self.typeshed_weak_cache.boolean.clone() + } + + pub fn get_ts_int(&mut self) -> Weak> { + if self.typeshed_weak_cache.int.is_expired() { + self.typeshed_weak_cache.int = Rc::downgrade(&self.get_symbol("", &(vec![Sy!("builtins")], vec![Sy!("int")]), u32::MAX).last().unwrap().clone()); + } + self.typeshed_weak_cache.int.clone() + } + + pub fn get_ts_float(&mut self) -> Weak> { + if self.typeshed_weak_cache.float.is_expired() { + self.typeshed_weak_cache.float = Rc::downgrade(&self.get_symbol("", &(vec![Sy!("builtins")], vec![Sy!("float")]), u32::MAX).last().unwrap().clone()); + } + self.typeshed_weak_cache.float.clone() + } + + pub fn get_ts_complex(&mut self) -> Weak> { + if self.typeshed_weak_cache.complex.is_expired() { + self.typeshed_weak_cache.complex = Rc::downgrade(&self.get_symbol("", &(vec![Sy!("builtins")], vec![Sy!("complex")]), u32::MAX).last().unwrap().clone()); + } + self.typeshed_weak_cache.complex.clone() + } + + pub fn get_ts_ellipsis(&mut self) -> Weak> { + if self.typeshed_weak_cache.ellipsis.is_expired() { + self.typeshed_weak_cache.ellipsis = Rc::downgrade(&self.get_symbol("", &(vec![Sy!("builtins")], vec![Sy!("Ellipsis")]), u32::MAX).last().unwrap().clone()); + } + self.typeshed_weak_cache.ellipsis.clone() + } + + pub fn get_ts_bytes(&mut self) -> Weak> { + if self.typeshed_weak_cache.bytes.is_expired() { + self.typeshed_weak_cache.bytes = Rc::downgrade(&self.get_symbol("", &(vec![Sy!("builtins")], vec![Sy!("bytes")]), u32::MAX).last().unwrap().clone()); + } + self.typeshed_weak_cache.bytes.clone() + } + + pub fn get_ts_object(&mut self) -> Weak> { + if self.typeshed_weak_cache.object.is_expired() { + self.typeshed_weak_cache.object = Rc::downgrade(&self.get_symbol("", &(vec![Sy!("builtins")], vec![Sy!("object")]), u32::MAX).last().unwrap().clone()); + } + self.typeshed_weak_cache.object.clone() + } + } #[derive(Debug)] diff --git a/server/src/core/symbols/class_symbol.rs b/server/src/core/symbols/class_symbol.rs index c59740bd..620eef06 100644 --- a/server/src/core/symbols/class_symbol.rs +++ b/server/src/core/symbols/class_symbol.rs @@ -8,6 +8,7 @@ use crate::constants::{OYarn, SymType}; use crate::core::file_mgr::NoqaInfo; use crate::core::model::ModelData; use crate::Sy; +use crate::utils::NoHashBuilder; use super::symbol::Symbol; use super::symbol_mgr::{SectionRange, SymbolMgr}; @@ -30,7 +31,7 @@ pub struct ClassSymbol { //Trait SymbolMgr //--- Body symbols pub sections: Vec, - pub symbols: HashMap>>>>, + pub symbols: HashMap>>, NoHashBuilder>>, //--- dynamics variables pub ext_symbols: HashMap>>>, pub decl_ext_symbols: PtrWeakKeyHashMap>, HashMap>>>>> @@ -84,7 +85,7 @@ impl ClassSymbol { } pub fn add_symbol(&mut self, content: &Rc>, section: u32) { - let sections = self.symbols.entry(content.borrow().name().clone()).or_insert(HashMap::new()); + let sections = self.symbols.entry(content.borrow().name().clone()).or_insert(HashMap::default()); let section_vec = sections.entry(section).or_insert(vec![]); section_vec.push(content.clone()); } diff --git a/server/src/core/symbols/file_symbol.rs b/server/src/core/symbols/file_symbol.rs index f73727a0..5f1d5eb5 100644 --- a/server/src/core/symbols/file_symbol.rs +++ b/server/src/core/symbols/file_symbol.rs @@ -1,6 +1,6 @@ use weak_table::{PtrWeakHashSet, PtrWeakKeyHashMap}; -use crate::{constants::{BuildStatus, BuildSteps, OYarn}, core::{file_mgr::NoqaInfo, model::Model, xml_data::OdooData}, oyarn}; +use crate::{constants::{BuildStatus, BuildSteps, OYarn}, core::{file_mgr::NoqaInfo, model::Model, xml_data::OdooData}, oyarn, utils::NoHashBuilder}; use std::{cell::RefCell, collections::HashMap, rc::{Rc, Weak}}; use super::{symbol::Symbol, symbol_mgr::{SectionRange, SymbolMgr}}; @@ -28,7 +28,7 @@ pub struct FileSymbol { //Trait SymbolMgr pub sections: Vec, - pub symbols: HashMap>>>>, + pub symbols: HashMap>>, NoHashBuilder>>, //--- dynamics variables pub ext_symbols: HashMap>>>, pub decl_ext_symbols: PtrWeakKeyHashMap>, HashMap>>>>> @@ -66,7 +66,7 @@ impl FileSymbol { } pub fn add_symbol(&mut self, content: &Rc>, section: u32) { - let sections = self.symbols.entry(content.borrow().name().clone()).or_insert_with(|| HashMap::new()); + let sections = self.symbols.entry(content.borrow().name().clone()).or_insert_with(|| HashMap::default()); let section_vec = sections.entry(section).or_insert_with(|| vec![]); section_vec.push(content.clone()); } diff --git a/server/src/core/symbols/function_symbol.rs b/server/src/core/symbols/function_symbol.rs index a5a87c71..e7ef1af0 100644 --- a/server/src/core/symbols/function_symbol.rs +++ b/server/src/core/symbols/function_symbol.rs @@ -5,7 +5,7 @@ use ruff_python_ast::{AtomicNodeIndex, Expr, ExprCall}; use ruff_text_size::{TextRange, TextSize}; use weak_table::{PtrWeakHashSet, PtrWeakKeyHashMap}; -use crate::{constants::{BuildStatus, BuildSteps, OYarn, SymType}, core::{evaluation::{Context, Evaluation}, file_mgr::NoqaInfo, model::Model}, oyarn, threads::SessionInfo}; +use crate::{constants::{BuildStatus, BuildSteps, OYarn, SymType}, core::{evaluation::{Context, Evaluation}, file_mgr::NoqaInfo, model::Model}, oyarn, threads::SessionInfo, utils::NoHashBuilder}; use super::{symbol::Symbol, symbol_mgr::{SectionRange, SymbolMgr}}; @@ -55,7 +55,7 @@ pub struct FunctionSymbol { //Trait SymbolMgr //--- Body content pub sections: Vec, - pub symbols: HashMap>>>>, + pub symbols: HashMap>>, NoHashBuilder>>, //--- dynamics variables pub ext_symbols: HashMap>>>, pub decl_ext_symbols: PtrWeakKeyHashMap>, HashMap>>>>> @@ -102,7 +102,7 @@ impl FunctionSymbol { } pub fn add_symbol(&mut self, content: &Rc>, section: u32) { - let sections = self.symbols.entry(content.borrow().name().clone()).or_insert(HashMap::new()); + let sections = self.symbols.entry(content.borrow().name().clone()).or_insert(HashMap::default()); let section_vec = sections.entry(section).or_insert(vec![]); section_vec.push(content.clone()); } diff --git a/server/src/core/symbols/module_symbol.rs b/server/src/core/symbols/module_symbol.rs index 21ea0d44..f5c971a2 100644 --- a/server/src/core/symbols/module_symbol.rs +++ b/server/src/core/symbols/module_symbol.rs @@ -18,7 +18,7 @@ use crate::core::odoo::SyncOdoo; use crate::core::symbols::symbol::Symbol; use crate::core::symbols::symbol_mgr::SymbolMgr; use crate::threads::SessionInfo; -use crate::utils::PathSanitizer as _; +use crate::utils::{NoHashBuilder, PathSanitizer as _}; use crate::S; use std::path::PathBuf; use std::rc::{Rc, Weak}; @@ -61,7 +61,7 @@ pub struct ModuleSymbol { //Trait SymbolMgr pub sections: Vec, - pub symbols: HashMap>>>>, + pub symbols: HashMap>>, NoHashBuilder>>, //--- dynamics variables pub ext_symbols: HashMap>>>, pub decl_ext_symbols: PtrWeakKeyHashMap>, HashMap>>>>>, @@ -136,7 +136,7 @@ impl ModuleSymbol { } pub fn add_symbol(&mut self, content: &Rc>, section: u32) { - let sections = self.symbols.entry(content.borrow().name().clone()).or_insert_with(|| HashMap::new()); + let sections = self.symbols.entry(content.borrow().name().clone()).or_insert_with(|| HashMap::default()); let section_vec = sections.entry(section).or_insert_with(|| vec![]); section_vec.push(content.clone()); } diff --git a/server/src/core/symbols/package_symbol.rs b/server/src/core/symbols/package_symbol.rs index 8a1798b8..7ab98157 100644 --- a/server/src/core/symbols/package_symbol.rs +++ b/server/src/core/symbols/package_symbol.rs @@ -1,6 +1,6 @@ use weak_table::{PtrWeakHashSet, PtrWeakKeyHashMap}; -use crate::{constants::{BuildStatus, BuildSteps, OYarn}, core::{file_mgr::NoqaInfo, model::Model, xml_data::OdooData}, oyarn, threads::SessionInfo, S}; +use crate::{S, constants::{BuildStatus, BuildSteps, OYarn}, core::{file_mgr::NoqaInfo, model::Model, xml_data::OdooData}, oyarn, threads::SessionInfo, utils::NoHashBuilder}; use std::{cell::RefCell, collections::HashMap, path::PathBuf, rc::{Rc, Weak}}; use super::{module_symbol::ModuleSymbol, symbol::Symbol, symbol_mgr::{SectionRange, SymbolMgr}}; @@ -120,7 +120,7 @@ pub struct PythonPackageSymbol { //Trait SymbolMgr pub sections: Vec, - pub symbols: HashMap>>>>, + pub symbols: HashMap>>, NoHashBuilder>>, //--- dynamics variables pub ext_symbols: HashMap>>>, pub decl_ext_symbols: PtrWeakKeyHashMap>, HashMap>>>>> @@ -159,7 +159,7 @@ impl PythonPackageSymbol { } pub fn add_symbol(&mut self, content: &Rc>, section: u32) { - let sections = self.symbols.entry(content.borrow().name().clone()).or_insert(HashMap::new()); + let sections = self.symbols.entry(content.borrow().name().clone()).or_insert(HashMap::default()); let section_vec = sections.entry(section).or_insert(vec![]); section_vec.push(content.clone()); } diff --git a/server/src/core/symbols/symbol.rs b/server/src/core/symbols/symbol.rs index fb10a712..8c06c0a6 100644 --- a/server/src/core/symbols/symbol.rs +++ b/server/src/core/symbols/symbol.rs @@ -13,7 +13,7 @@ use crate::core::evaluation::{Context, ContextValue, Evaluation, EvaluationSymbo use crate::core::model::Model; use crate::core::odoo::SyncOdoo; use crate::threads::SessionInfo; -use crate::utils::{compare_semver, PathSanitizer as _}; +use crate::utils::{NoHashBuilder, PathSanitizer as _, compare_semver}; use crate::S; use core::panic; use std::cmp::Ordering; @@ -1121,7 +1121,7 @@ impl Symbol { } } - pub fn iter_symbols(&self) -> std::collections::hash_map::Iter<'_, OYarn, HashMap>>>> { + pub fn iter_symbols(&self) -> std::collections::hash_map::Iter<'_, OYarn, HashMap>>, NoHashBuilder>> { match self { Symbol::File(f) => { f.symbols.iter() diff --git a/server/src/core/symbols/symbol_mgr.rs b/server/src/core/symbols/symbol_mgr.rs index f6777416..58a2fbb5 100644 --- a/server/src/core/symbols/symbol_mgr.rs +++ b/server/src/core/symbols/symbol_mgr.rs @@ -3,6 +3,7 @@ use std::{cell::RefCell, rc::Rc, collections::{HashMap, HashSet}}; use ruff_text_size::TextSize; use crate::constants::OYarn; +use crate::utils::NoHashBuilder; use super::{class_symbol::ClassSymbol, file_symbol::FileSymbol, function_symbol::FunctionSymbol, module_symbol::ModuleSymbol, package_symbol::PythonPackageSymbol, symbol::Symbol}; @@ -34,7 +35,7 @@ pub trait SymbolMgr { fn change_parent(&mut self, new_parent: SectionIndex, section: &mut SectionRange); fn get_content_symbol(&self, name: OYarn, position: u32) -> ContentSymbols; fn _init_symbol_mgr(&mut self); - fn _get_loc_symbol(&self, map: &HashMap>>>, position: u32, index: &SectionIndex, acc: &mut HashSet) -> ContentSymbols; + fn _get_loc_symbol(&self, map: &HashMap>>, NoHashBuilder>, position: u32, index: &SectionIndex, acc: &mut HashSet) -> ContentSymbols; fn get_all_visible_symbols(&self, name_prefix: &String, position: u32) -> HashMap>>>; } @@ -101,7 +102,7 @@ macro_rules! impl_section_mgr_for { ///Return all the symbols that are valid as last declaration for the given position fn get_content_symbol(&self, name: OYarn, position: u32) -> ContentSymbols { - let sections: Option<&HashMap>>>> = self.symbols.get(&name); + let sections: Option<&HashMap>>, NoHashBuilder>> = self.symbols.get(&name); let mut content = if let Some(sections) = sections { let section: SectionRange = self.get_section_for(position); self._get_loc_symbol(sections, position, &SectionIndex::INDEX(section.index), &mut HashSet::new()) @@ -117,7 +118,7 @@ macro_rules! impl_section_mgr_for { } ///given all the sections of a symbol and a position, return all the Symbols that can represent the symbol - fn _get_loc_symbol(&self, map: &HashMap>>>, position: u32, index: &SectionIndex, acc: &mut HashSet) -> ContentSymbols { + fn _get_loc_symbol(&self, map: &HashMap>>, NoHashBuilder>, position: u32, index: &SectionIndex, acc: &mut HashSet) -> ContentSymbols { let mut res = ContentSymbols::default(); match index { SectionIndex::NONE => { return res; }, diff --git a/server/src/features/references.rs b/server/src/features/references.rs index 7b406ae7..4f33b019 100644 --- a/server/src/features/references.rs +++ b/server/src/features/references.rs @@ -13,7 +13,6 @@ use crate::core::odoo::SyncOdoo; use crate::features::goto_utils::{GotoRequest, GotoSourceType, GotoUtils}; use crate::{constants::SymType, core::{file_mgr::{FileInfo, FileMgr}, symbols::symbol::Symbol}, features::xml_ast_utils::{XmlAstResult, XmlAstUtils}, threads::SessionInfo, utils::PathSanitizer}; - pub struct ReferenceFeature { } @@ -26,6 +25,7 @@ impl ReferenceFeature { /// TODO: Odoo specific (XML field refs, string-based model refs) pub fn get_references(session: &mut SessionInfo, file_symbol: &Rc>, file_info: &Rc>, line: u32, character: u32) -> Option> { //We want to search for references of the definition, and not the current symbol. Let's use definition feature for that + SyncOdoo::process_rebuilds(session, false); let def_sources = GotoUtils::get_symbols(session, GotoRequest::Definition, file_symbol, file_info, line, character); let mut locations = Vec::new(); @@ -80,7 +80,6 @@ impl ReferenceFeature { } fn references_in_file(session: &mut SessionInfo, file_symbol: &Rc>, file_info: &Rc>, symbol_name: &String, target_symbol_rc: &Rc>) -> Vec { - SyncOdoo::process_rebuilds(session, false); error!("Searching references for symbol {} in file {}", symbol_name, file_symbol.borrow().paths()[0]); let file_info_ast = file_info.borrow().file_info_ast.clone(); let mut visitor = ReferenceVisitor { diff --git a/server/src/server.rs b/server/src/server.rs index efb12b4e..3d374273 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -179,7 +179,7 @@ impl Server { references_provider: Some(OneOf::Right(ReferencesOptions { work_done_progress_options: WorkDoneProgressOptions { work_done_progress: Some(false) - } + }, })), document_symbol_provider: Some(OneOf::Right(DocumentSymbolOptions{ label: Some(S!("Odoo")), diff --git a/server/src/utils.rs b/server/src/utils.rs index 6e9e585b..fe032b50 100644 --- a/server/src/utils.rs +++ b/server/src/utils.rs @@ -374,3 +374,27 @@ macro_rules! warn_or_panic { } } } + +use std::hash::{BuildHasherDefault, Hasher}; +#[derive(Default)] +pub struct U32IdentityHasher(u64); + +impl Hasher for U32IdentityHasher { + fn write(&mut self, bytes: &[u8]) { + // fallback lent (rare si on n'utilise que u32) + let mut v = 0u64; + for (i, b) in bytes.iter().enumerate().take(8) { + v |= (*b as u64) << (i * 8); + } + self.0 = v; + } + + fn write_u32(&mut self, i: u32) { + self.0 = i as u64; + } + + fn finish(&self) -> u64 { + self.0 + } +} +pub type NoHashBuilder = BuildHasherDefault; \ No newline at end of file From a6fc73bfa904d61f3a487678897c42821143d90a Mon Sep 17 00:00:00 2001 From: fda-odoo Date: Thu, 19 Feb 2026 14:19:55 +0100 Subject: [PATCH 5/9] [IMP] improve searches performances by avoiding useless searches in models --- server/src/features/references.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/src/features/references.rs b/server/src/features/references.rs index 4f33b019..87fa7cb0 100644 --- a/server/src/features/references.rs +++ b/server/src/features/references.rs @@ -1,3 +1,4 @@ +use std::rc::Weak; use std::{cell::RefCell, path::PathBuf, rc::Rc}; use lsp_types::{Location, Range}; @@ -5,6 +6,7 @@ use ruff_python_ast::visitor::{walk_expr, walk_stmt, Visitor}; use ruff_python_ast::{Alias, Expr, Identifier, Stmt, StmtAnnAssign, StmtAssert, StmtAssign, StmtAugAssign, StmtClassDef, StmtIf, StmtMatch, StmtRaise, StmtReturn, StmtTry, StmtTypeAlias, StmtWith}; use ruff_text_size::{Ranged, TextRange, TextSize}; use tracing::error; +use weak_table::PtrWeakHashSet; use crate::S; use crate::constants::OYarn; @@ -29,6 +31,7 @@ impl ReferenceFeature { let def_sources = GotoUtils::get_symbols(session, GotoRequest::Definition, file_symbol, file_info, line, character); let mut locations = Vec::new(); + let mut already_processed: PtrWeakHashSet>> = PtrWeakHashSet::new(); for definition in def_sources.iter() { match &definition.source { GotoSourceType::Symbol(target_symbol) => { @@ -54,6 +57,10 @@ impl ReferenceFeature { if target_symbol.borrow().typ() == SymType::CLASS { if let Some(model_data) = target_symbol.borrow().as_class_sym()._model.as_ref() { if let Some(model) = session.sync_odoo.models.get(&model_data.name).cloned() { + let main_sym = model.borrow().get_main_symbols(session, target_symbol.borrow().find_module()); + if main_sym.len() == 1 && !Rc::ptr_eq(target_symbol, &main_sym[0]) { + continue; + } let dependents = model.borrow().dependents.clone(); for dep_symbol_rc in dependents.iter() { let Some(Some(file)) = dep_symbol_rc.borrow().get_file().map(|x| x.upgrade()) else { //to be sure we are on a file @@ -81,6 +88,9 @@ impl ReferenceFeature { fn references_in_file(session: &mut SessionInfo, file_symbol: &Rc>, file_info: &Rc>, symbol_name: &String, target_symbol_rc: &Rc>) -> Vec { error!("Searching references for symbol {} in file {}", symbol_name, file_symbol.borrow().paths()[0]); + if file_symbol.borrow().paths()[0].contains("test_event_sale_with_product_configurator") { + error!("found one"); + } let file_info_ast = file_info.borrow().file_info_ast.clone(); let mut visitor = ReferenceVisitor { sym_stack: vec![], From aaf65d403bedc8d6faab7f32d5f8d3307600f88f Mon Sep 17 00:00:00 2001 From: fda-odoo Date: Thu, 19 Feb 2026 14:20:54 +0100 Subject: [PATCH 6/9] [IMP] iai_profiler to gungraun --- server/Cargo.toml | 4 ++-- .../{iai_profiler.rs => gungraun_bench.rs} | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) rename server/benches/{iai_profiler.rs => gungraun_bench.rs} (87%) diff --git a/server/Cargo.toml b/server/Cargo.toml index 1c13b7a2..7f4a263f 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -46,11 +46,11 @@ csv = "1.3.1" nix = { version = "0.29.0", features = ["process"] } [[bench]] -name = "iai_profiler" +name = "gungraun_bench" harness = false [dev-dependencies] -iai-callgrind = "0.14.2" +gungraun = "0.17.2" assert_fs = "1.1" [features] diff --git a/server/benches/iai_profiler.rs b/server/benches/gungraun_bench.rs similarity index 87% rename from server/benches/iai_profiler.rs rename to server/benches/gungraun_bench.rs index a847bb07..1ba715d7 100644 --- a/server/benches/iai_profiler.rs +++ b/server/benches/gungraun_bench.rs @@ -8,20 +8,20 @@ use tracing_subscriber::{fmt, FmtSubscriber, layer::SubscriberExt}; use std::env; -use iai_callgrind::{ - library_benchmark, library_benchmark_group, main, LibraryBenchmarkConfig, - FlamegraphConfig +use gungraun::{ + Callgrind, FlamegraphConfig, LibraryBenchmarkConfig, library_benchmark, library_benchmark_group, main }; use std::hint::black_box; /* -To run iai-callgrind: +To run gungraun-callgrind: install valgrind -run cargo install --version 0.14.0 iai-callgrind-runner +run cargo install --version 0.14.0 gungraun-runner +run cargo bench -- --nocapture */ #[library_benchmark] -fn iai_main() { +fn gungraun_main() { // TODO: Audit that the environment access only happens in single-threaded code. unsafe { env::set_var("RUST_BACKTRACE", "full") }; @@ -87,9 +87,11 @@ fn iai_main() { info!(">>>>>>>>>>>>>>>>>> End Session <<<<<<<<<<<<<<<<<<"); } -library_benchmark_group!(name = my_group; benchmarks = iai_main); +library_benchmark_group!(name = my_group; benchmarks = gungraun_main); main!( - config = LibraryBenchmarkConfig::default().flamegraph(FlamegraphConfig::default()); + config = LibraryBenchmarkConfig::default().tool(Callgrind::default() + .flamegraph(FlamegraphConfig::default()) + ), library_benchmark_groups = my_group ); \ No newline at end of file From 7013b1bbe1eb5da9c662d1fc7009aa0638a1b91d Mon Sep 17 00:00:00 2001 From: fda-odoo Date: Thu, 19 Feb 2026 14:31:02 +0100 Subject: [PATCH 7/9] [IMP] various compiler warning fixes --- server/src/core/evaluation.rs | 5 ++- server/src/core/symbols/symbol.rs | 2 +- server/src/features/declaration.rs | 4 +-- server/src/features/definition.rs | 4 +-- server/src/features/goto_utils.rs | 49 +++++++++++++++--------------- server/src/features/references.rs | 34 --------------------- server/src/threads.rs | 2 +- 7 files changed, 30 insertions(+), 70 deletions(-) diff --git a/server/src/core/evaluation.rs b/server/src/core/evaluation.rs index 08b92daa..603c36ff 100644 --- a/server/src/core/evaluation.rs +++ b/server/src/core/evaluation.rs @@ -3,7 +3,6 @@ use itertools::FoldWhile::{Continue, Done}; use ruff_python_ast::{Arguments, Expr, ExprCall, Identifier, Number, Operator, Parameter, UnaryOp}; use ruff_text_size::{Ranged, TextRange, TextSize}; use lsp_types::{Diagnostic, Location, Position, Range}; -use tracing::error; use weak_table::traits::WeakElement; use std::cmp::{max, min}; use std::collections::{HashMap, HashSet}; @@ -1771,10 +1770,10 @@ impl Evaluation { if arg.is_starred_expr() { continue; } - let (_, diags) = Evaluation::eval_from_ast(session, arg, parent.clone(), &expr_call.range.start(), false, &mut vec![]); + Evaluation::eval_from_ast(session, arg, parent.clone(), &expr_call.range.start(), false, &mut vec![]); } for arg in expr_call.arguments.keywords.iter() { - let (_, diags) = Evaluation::eval_from_ast(session, &arg.value, parent.clone(), &expr_call.range.start(), false, &mut vec![]); + Evaluation::eval_from_ast(session, &arg.value, parent.clone(), &expr_call.range.start(), false, &mut vec![]); } } } diff --git a/server/src/core/symbols/symbol.rs b/server/src/core/symbols/symbol.rs index 8c06c0a6..e9149b17 100644 --- a/server/src/core/symbols/symbol.rs +++ b/server/src/core/symbols/symbol.rs @@ -2849,7 +2849,7 @@ impl Symbol { ) -> (Vec>>, Vec) { let mut result: Vec>> = vec![]; let mut visited_symbols: PtrWeakHashSet>> = PtrWeakHashSet::new(); - let mut extend_result = |syms: Vec>>, result: &mut Vec>>, visited_symbols: &mut PtrWeakHashSet>>| { + let extend_result = |syms: Vec>>, result: &mut Vec>>, visited_symbols: &mut PtrWeakHashSet>>| { syms.iter().for_each(|sym|{ if !visited_symbols.contains(sym){ visited_symbols.insert(sym.clone()); diff --git a/server/src/features/declaration.rs b/server/src/features/declaration.rs index 5f4086d3..49dc7a36 100644 --- a/server/src/features/declaration.rs +++ b/server/src/features/declaration.rs @@ -24,9 +24,7 @@ impl DeclarationFeature { let definitions_sources = GotoUtils::get_symbols(session, GotoRequest::Declaration, file_symbol, file_info, line, character); let mut links = vec![]; for def in definitions_sources.iter() { - if let Some(link) = GotoUtils::goto_source_to_location(session, def) { - links.push(link); - } + links.extend(GotoUtils::goto_source_to_location(session, def)); } Some(GotoDeclarationResponse::Link(links)) } diff --git a/server/src/features/definition.rs b/server/src/features/definition.rs index 3c6d8eb5..151f8346 100644 --- a/server/src/features/definition.rs +++ b/server/src/features/definition.rs @@ -23,9 +23,7 @@ impl DefinitionFeature { let definitions_sources = GotoUtils::get_symbols(session, GotoRequest::Definition, file_symbol, file_info, line, character); let mut links = vec![]; for def in definitions_sources.iter() { - if let Some(link) = GotoUtils::goto_source_to_location(session, def) { - links.push(link); - } + links.extend(GotoUtils::goto_source_to_location(session, def)); } Some(GotoDefinitionResponse::Link(links)) } diff --git a/server/src/features/goto_utils.rs b/server/src/features/goto_utils.rs index 5a9095df..6ddbc683 100644 --- a/server/src/features/goto_utils.rs +++ b/server/src/features/goto_utils.rs @@ -125,7 +125,7 @@ impl GotoUtils { for xml_id in xml_ids { let file = xml_id.get_file_symbol(); if let Some(file) = file { - if let Some(file) = file.upgrade() { + if let Some(_file) = file.upgrade() { //test that file is valid to set xml_found to true xml_found = true; sources.push(GotoSource{ source: GotoSourceType::OdooData(xml_id.clone()), @@ -182,14 +182,10 @@ impl GotoUtils { symbol.borrow().get_member_symbol(session, &S!("_compute_display_name"), maybe_module.clone(), false, false, true, true, false).0 }).collect::>(); for symbol in symbols { - if let Some(file) = symbol.borrow().get_file() { - for path in file.upgrade().unwrap().borrow().paths().iter() { - sources.push(GotoSource { - source: GotoSourceType::Symbol(symbol.clone()), - origin_selection_range: None, - }); - } - } + sources.push(GotoSource { + source: GotoSourceType::Symbol(symbol.clone()), + origin_selection_range: None, + }); } } @@ -273,27 +269,30 @@ impl GotoUtils { definition_sources } - pub fn goto_source_to_location(session: &mut SessionInfo, def: &GotoSource) -> Option { + pub fn goto_source_to_location(session: &mut SessionInfo, def: &GotoSource) -> Vec { + let mut res = vec![]; match &def.source { GotoSourceType::Symbol(symbol) => { if let Some(file_symbol) = symbol.borrow().get_file().and_then(|file_sym_weak| file_sym_weak.upgrade()){ - let path = file_symbol.borrow().get_symbol_first_path(); - let symbol_range = if symbol.borrow().has_range() { - session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, &path, &symbol.borrow().range()) - } else { - Range::default() - }; - return Some(LocationLink{ - origin_selection_range: def.origin_selection_range.clone(), - target_uri: FileMgr::pathname2uri(&path), - target_selection_range: symbol_range, - target_range: symbol_range, - }); + let paths = file_symbol.borrow().paths(); + for path in paths.iter() { + let symbol_range = if symbol.borrow().has_range() { + session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, &path, &symbol.borrow().range()) + } else { + Range::default() + }; + res.push(LocationLink{ + origin_selection_range: def.origin_selection_range.clone(), + target_uri: FileMgr::pathname2uri(&path), + target_selection_range: symbol_range, + target_range: symbol_range, + }); + } } }, GotoSourceType::Module(module) => { let path = PathBuf::from(module.borrow().paths()[0].clone()).join("__manifest__.py").sanitize(); - return Some(LocationLink{ + res.push(LocationLink{ origin_selection_range: None, target_uri: FileMgr::pathname2uri(&path), target_selection_range: Range::default(), @@ -305,7 +304,7 @@ impl GotoUtils { if let Some(file) = file { if let Some(file) = file.upgrade() { let range = session.sync_odoo.get_file_mgr().borrow().std_range_to_range(session, &file.borrow().paths()[0], &xml_id.get_range()); - return Some(LocationLink { + res.push(LocationLink { origin_selection_range: def.origin_selection_range.clone(), target_uri: FileMgr::pathname2uri(&file.borrow().paths()[0]), target_range: range, @@ -314,6 +313,6 @@ impl GotoUtils { } } } - None + res } } \ No newline at end of file diff --git a/server/src/features/references.rs b/server/src/features/references.rs index 87fa7cb0..d015c81b 100644 --- a/server/src/features/references.rs +++ b/server/src/features/references.rs @@ -1,12 +1,9 @@ -use std::rc::Weak; use std::{cell::RefCell, path::PathBuf, rc::Rc}; use lsp_types::{Location, Range}; -use ruff_python_ast::visitor::{walk_expr, walk_stmt, Visitor}; use ruff_python_ast::{Alias, Expr, Identifier, Stmt, StmtAnnAssign, StmtAssert, StmtAssign, StmtAugAssign, StmtClassDef, StmtIf, StmtMatch, StmtRaise, StmtReturn, StmtTry, StmtTypeAlias, StmtWith}; use ruff_text_size::{Ranged, TextRange, TextSize}; use tracing::error; -use weak_table::PtrWeakHashSet; use crate::S; use crate::constants::OYarn; @@ -31,7 +28,6 @@ impl ReferenceFeature { let def_sources = GotoUtils::get_symbols(session, GotoRequest::Definition, file_symbol, file_info, line, character); let mut locations = Vec::new(); - let mut already_processed: PtrWeakHashSet>> = PtrWeakHashSet::new(); for definition in def_sources.iter() { match &definition.source { GotoSourceType::Symbol(target_symbol) => { @@ -102,36 +98,6 @@ impl ReferenceFeature { std::mem::take(&mut session.sync_odoo.evaluation_locations) } - fn is_same_or_imported(session: &mut SessionInfo, symbol: &Rc>, target_symbol: &Rc>) -> bool { - if Rc::ptr_eq(symbol, target_symbol) { - return true; - } - - // Check if symbol is an imported symbol that points to the target symbol - if symbol.borrow().typ() != SymType::VARIABLE { - return false; - } - if symbol.borrow().as_variable().is_import_variable { - if symbol.borrow().evaluations().as_ref().unwrap().is_empty() { - return false; - } - let file_symbol = symbol.borrow().get_file().unwrap().upgrade().unwrap(); - for eval in symbol.borrow().evaluations().as_ref().unwrap().iter() { - let eval_ptr = eval.symbol.get_symbol(session, &mut None, &mut vec![], Some(file_symbol.clone())); - match eval_ptr { - EvaluationSymbolPtr::WEAK(w) => { - if let Some(sym_upg) = w.weak.upgrade() { - return ReferenceFeature::is_same_or_imported(session, &sym_upg, target_symbol) - } - }, - _ => {continue;} - } - } - } - - false - } - pub fn get_references_xml(session: &mut SessionInfo, file_symbol: &Rc>, file_info: &Rc>, line: u32, character: u32) -> Option> { let offset = file_info.borrow().position_to_offset(line, character, session.sync_odoo.encoding); let data = file_info.borrow().file_info_ast.borrow().text_document.as_ref().unwrap().contents().to_string(); diff --git a/server/src/threads.rs b/server/src/threads.rs index 2aa62308..b799c391 100644 --- a/server/src/threads.rs +++ b/server/src/threads.rs @@ -4,7 +4,7 @@ use crossbeam_channel::{Receiver, Sender, TryRecvError}; use lsp_server::{Message, RequestId, Response, ResponseError}; use lsp_types::{CompletionResponse, DocumentSymbolResponse, GotoDefinitionResponse, Hover, Location, LogMessageParams, MessageType, ShowMessageParams, WorkspaceSymbol, WorkspaceSymbolResponse, notification::{DidChangeConfiguration, DidChangeTextDocument, DidChangeWatchedFiles, DidChangeWorkspaceFolders, DidCloseTextDocument, DidCreateFiles, DidDeleteFiles, DidOpenTextDocument, DidRenameFiles, DidSaveTextDocument, LogMessage, - Notification, ShowMessage}, request::{Completion, DocumentSymbolRequest, GotoDeclaration, GotoDeclarationResponse, GotoDefinition, GotoTypeDefinitionResponse, HoverRequest, References, Request, Shutdown, WorkspaceSymbolRequest, WorkspaceSymbolResolve}}; + Notification, ShowMessage}, request::{Completion, DocumentSymbolRequest, GotoDeclaration, GotoDeclarationResponse, GotoDefinition, HoverRequest, References, Request, Shutdown, WorkspaceSymbolRequest, WorkspaceSymbolResolve}}; use serde::{de::DeserializeOwned, Serialize}; use serde_json::Value; use tracing::{error, info, warn}; From 8ab3068de3387c2bb3d0c0266822c297d7209106 Mon Sep 17 00:00:00 2001 From: fda-odoo Date: Thu, 19 Feb 2026 17:03:08 +0100 Subject: [PATCH 8/9] [ FIX] tests + remove duplicated references with models --- server/src/core/evaluation.rs | 49 ++++++++++++++--- server/src/core/symbols/symbol.rs | 28 ++++++++++ server/src/features/goto_utils.rs | 10 ++-- server/src/features/references.rs | 57 ++++++++++---------- server/tests/test_get_symbol.rs | 53 +------------------ server/tests/test_references.rs | 88 +++++++++++++++++++++++++++++++ server/tests/test_utils.rs | 11 ---- 7 files changed, 197 insertions(+), 99 deletions(-) create mode 100644 server/tests/test_references.rs diff --git a/server/src/core/evaluation.rs b/server/src/core/evaluation.rs index 603c36ff..6c73df06 100644 --- a/server/src/core/evaluation.rs +++ b/server/src/core/evaluation.rs @@ -3,6 +3,7 @@ use itertools::FoldWhile::{Continue, Done}; use ruff_python_ast::{Arguments, Expr, ExprCall, Identifier, Number, Operator, Parameter, UnaryOp}; use ruff_text_size::{Ranged, TextRange, TextSize}; use lsp_types::{Diagnostic, Location, Position, Range}; +use tracing::error; use weak_table::traits::WeakElement; use std::cmp::{max, min}; use std::collections::{HashMap, HashSet}; @@ -1176,7 +1177,7 @@ impl Evaluation { let bases = Symbol::follow_ref(&base, session, &mut None, false, false, None, None); let value = Evaluation::expr_to_str(session, &sub.slice, parent.clone(), max_infer, false, &mut diagnostics); diagnostics.extend(value.1); - for base in bases.iter() { + for base in bases.iter() { match base { EvaluationSymbolPtr::WEAK(base_sym_weak_eval) if base_sym_weak_eval.instance == Some(false) => { if let Some(SymType::CLASS) = base.upgrade_weak().map(|s| s.borrow().typ()) { @@ -1370,14 +1371,50 @@ impl Evaluation { if let Some(file_info) = file_info { found_one_reference = true; let range= ast.range(); - let tranformed_range = file_info.borrow().text_range_to_range(&range, session.sync_odoo.encoding); - session.sync_odoo.evaluation_locations.push(Location { - uri: FileMgr::pathname2uri(&file.borrow().paths().first().unwrap()), - range: tranformed_range, - }); + let transformed_range = file_info.borrow().text_range_to_range(&range, session.sync_odoo.encoding); + //check that a previous call to analyze_ast (recursively for ex) didn't already add this expression + let uri = FileMgr::pathname2uri(&file.borrow().paths().first().unwrap()); + if session.sync_odoo.evaluation_locations.is_empty() || + session.sync_odoo.evaluation_locations.last().unwrap().uri != uri || + session.sync_odoo.evaluation_locations.last().unwrap().range.start.line != transformed_range.start.line{ + session.sync_odoo.evaluation_locations.push(Location { + uri: uri, + range: transformed_range, + }); + } } } } + if let Some(value) = eval.value.as_ref() { + match value { + EvaluationValue::CONSTANT(constant) => { + match constant { + Expr::StringLiteral(s) => { + if evaluation_search.borrow().typ() == SymType::CLASS { + let class_bw = evaluation_search.borrow(); + let class = class_bw.as_class_sym(); + if let Some(model_data) = class._model.as_ref() { + if s.value.to_str() == model_data.name { + let file = parent.borrow().get_file().unwrap().upgrade().unwrap(); + let file_info = session.sync_odoo.get_file_mgr().borrow().get_file_info(&file.borrow().paths()[0]); + if let Some(file_info) = file_info { + let transformed_range = file_info.borrow().text_range_to_range(&s.range, session.sync_odoo.encoding); + let uri = FileMgr::pathname2uri(&file.borrow().paths().first().unwrap()); + session.sync_odoo.evaluation_locations.push(Location { + uri: uri, + range: transformed_range, + }); + } + } + } + } + }, + _ => {} + } + }, + _ => {} + } + } } } AnalyzeAstResult { evaluations: evals, diagnostics } diff --git a/server/src/core/symbols/symbol.rs b/server/src/core/symbols/symbol.rs index e9149b17..6af893f1 100644 --- a/server/src/core/symbols/symbol.rs +++ b/server/src/core/symbols/symbol.rs @@ -2352,6 +2352,34 @@ impl Symbol { results } + pub fn follow_imported_ref(evaluation: &EvaluationSymbolPtr, session: &mut SessionInfo, context: &mut Option) -> Vec { + let mut res = vec![]; + let mut symbols = VecDeque::new(); + symbols.push_back(evaluation.clone()); + while let Some(current_sym) = symbols.pop_front() { + let EvaluationSymbolPtr::WEAK(w) = ¤t_sym else { + res.push(current_sym.clone()); + continue; + }; + let Some(symbol) = w.weak.upgrade() else { + res.push(current_sym.clone()); + continue; + }; + if symbol.borrow().typ() == SymType::VARIABLE && symbol.borrow().as_variable().is_import_variable { + let sym_b = symbol.borrow(); + let evaluations = sym_b.evaluations(); + if let Some(evals) = evaluations { + for eval in evals.iter() { + symbols.push_back(eval.symbol.get_symbol(session, context, &mut vec![], None)); + } + } + } else { + res.push(current_sym); + } + } + res + } + pub fn all_symbols(&self) -> impl Iterator>> + use<> { //return an iterator on all symbols of self. only symbols in symbols and module_symbols will //be returned. diff --git a/server/src/features/goto_utils.rs b/server/src/features/goto_utils.rs index 6ddbc683..42cc8ca0 100644 --- a/server/src/features/goto_utils.rs +++ b/server/src/features/goto_utils.rs @@ -245,7 +245,7 @@ impl GotoUtils { } if matches!(goto_request, GotoRequest::Definition) { let eval_ptr = eval.symbol.get_symbol(session, &mut None, &mut vec![], None); - let end_symbols = Symbol::follow_ref(&eval_ptr, session, &mut None, true, false, None, None); + let end_symbols = Symbol::follow_imported_ref(&eval_ptr, session, &mut None); for end_symbol in end_symbols.iter() { if let Some(symbol) = end_symbol.upgrade_weak() { definition_sources.push(GotoSource{ @@ -276,14 +276,18 @@ impl GotoUtils { if let Some(file_symbol) = symbol.borrow().get_file().and_then(|file_sym_weak| file_sym_weak.upgrade()){ let paths = file_symbol.borrow().paths(); for path in paths.iter() { + let full_path = match file_symbol.borrow().typ() { + SymType::PACKAGE(_) => PathBuf::from(path).join(format!("__init__.py{}", file_symbol.borrow().as_package().i_ext())).sanitize(), + _ => path.clone() + }; let symbol_range = if symbol.borrow().has_range() { - session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, &path, &symbol.borrow().range()) + session.sync_odoo.get_file_mgr().borrow().text_range_to_range(session, &full_path, &symbol.borrow().range()) } else { Range::default() }; res.push(LocationLink{ origin_selection_range: def.origin_selection_range.clone(), - target_uri: FileMgr::pathname2uri(&path), + target_uri: FileMgr::pathname2uri(&full_path), target_selection_range: symbol_range, target_range: symbol_range, }); diff --git a/server/src/features/references.rs b/server/src/features/references.rs index d015c81b..4ff4c5b8 100644 --- a/server/src/features/references.rs +++ b/server/src/features/references.rs @@ -1,9 +1,11 @@ +use std::rc::Weak; use std::{cell::RefCell, path::PathBuf, rc::Rc}; use lsp_types::{Location, Range}; use ruff_python_ast::{Alias, Expr, Identifier, Stmt, StmtAnnAssign, StmtAssert, StmtAssign, StmtAugAssign, StmtClassDef, StmtIf, StmtMatch, StmtRaise, StmtReturn, StmtTry, StmtTypeAlias, StmtWith}; use ruff_text_size::{Ranged, TextRange, TextSize}; use tracing::error; +use weak_table::PtrWeakHashSet; use crate::S; use crate::constants::OYarn; @@ -31,21 +33,19 @@ impl ReferenceFeature { for definition in def_sources.iter() { match &definition.source { GotoSourceType::Symbol(target_symbol) => { + let mut to_check: PtrWeakHashSet>> = PtrWeakHashSet::new(); let symbol_name = target_symbol.borrow().name().to_string(); locations.extend(ReferenceFeature::references_in_file(session, file_symbol, file_info, &symbol_name, &target_symbol)); + to_check.insert(file_symbol.clone()); //take arch and arch_eval dependents - for dep in file_symbol.borrow().dependents()[0].iter().take(2) { - if let Some(dep_set) = dep { - for dep_symbol_rc in dep_set.iter() { - let Some(Some(file)) = dep_symbol_rc.borrow().get_file().map(|x| x.upgrade()) else { //to be sure we are on a file - continue; - }; - let Some(dep_file_info) = session.sync_odoo.get_file_mgr().borrow().get_file_info(&file.borrow().paths()[0]) else { - continue; - }; - locations.extend(ReferenceFeature::references_in_file(session, &dep_symbol_rc, &dep_file_info, &symbol_name, &target_symbol)); + if !file_symbol.borrow().dependents().is_empty() { // file could be out of workspace + for dep in file_symbol.borrow().dependents()[0].iter().take(2) { + if let Some(dep_set) = dep { + for dep_symbol_rc in dep_set.iter() { + to_check.insert(dep_symbol_rc.clone()); + } } } } @@ -53,23 +53,22 @@ impl ReferenceFeature { if target_symbol.borrow().typ() == SymType::CLASS { if let Some(model_data) = target_symbol.borrow().as_class_sym()._model.as_ref() { if let Some(model) = session.sync_odoo.models.get(&model_data.name).cloned() { - let main_sym = model.borrow().get_main_symbols(session, target_symbol.borrow().find_module()); - if main_sym.len() == 1 && !Rc::ptr_eq(target_symbol, &main_sym[0]) { - continue; - } - let dependents = model.borrow().dependents.clone(); - for dep_symbol_rc in dependents.iter() { - let Some(Some(file)) = dep_symbol_rc.borrow().get_file().map(|x| x.upgrade()) else { //to be sure we are on a file - continue; - }; - let Some(dep_file_info) = session.sync_odoo.get_file_mgr().borrow().get_file_info(&file.borrow().paths()[0]) else { - continue; - }; - locations.extend(ReferenceFeature::references_in_file(session, &dep_symbol_rc, &dep_file_info, &symbol_name, &target_symbol)); + to_check.extend(model.borrow().dependents.clone()); + for symbol in model.borrow().all_symbols(session, None, false) { + to_check.insert(symbol.0); } } } } + for sym in to_check.iter() { + let Some(Some(file)) = sym.borrow().get_file().map(|x| x.upgrade()) else { //to be sure we are on a file + continue; + }; + let Some(dep_file_info) = session.sync_odoo.get_file_mgr().borrow().get_file_info(&file.borrow().paths()[0]) else { + continue; + }; + locations.extend(ReferenceFeature::references_in_file(session, &sym, &dep_file_info, &symbol_name, &target_symbol)); + } }, _ => continue, } @@ -83,10 +82,6 @@ impl ReferenceFeature { } fn references_in_file(session: &mut SessionInfo, file_symbol: &Rc>, file_info: &Rc>, symbol_name: &String, target_symbol_rc: &Rc>) -> Vec { - error!("Searching references for symbol {} in file {}", symbol_name, file_symbol.borrow().paths()[0]); - if file_symbol.borrow().paths()[0].contains("test_event_sale_with_product_configurator") { - error!("found one"); - } let file_info_ast = file_info.borrow().file_info_ast.clone(); let mut visitor = ReferenceVisitor { sym_stack: vec![], @@ -338,6 +333,14 @@ impl ReferenceVisitor { } fn visit_assign(&mut self, session: &mut SessionInfo, assign: &StmtAssign) { + match *assign.value { + Expr::StringLiteral(ref s) => { + if s.value.to_str() == "pygls.tests.base_test_model" { + error!("here"); + } + }, + _=> {} + } self.visit_expr(session, &assign.value, &assign.range.start()); } diff --git a/server/tests/test_get_symbol.rs b/server/tests/test_get_symbol.rs index 39d3c62d..c85450b1 100644 --- a/server/tests/test_get_symbol.rs +++ b/server/tests/test_get_symbol.rs @@ -278,55 +278,4 @@ fn test_model_subscription() { "Resolving a subscript of a model should include the model symbol itself. Expected to find BaseTestModel symbol among resolved symbols of `partner = self.search([], limit=2)[-1:]`" ) -} - -#[test] -fn test_references() { - // setup - let (mut odoo, config) = setup::setup::setup_server(true); - let test_addons_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests").join("data").join("addons"); - let module1_test_file = test_addons_path.join("module_1").join("models").join("base_test_models.py").sanitize(); - - // test file exists - assert!(PathBuf::from(&module1_test_file).exists(), "Test file does not exist: {}", module1_test_file); - let mut session = setup::setup::create_init_session(&mut odoo, config); - - let file_mgr = session.sync_odoo.get_file_mgr(); - let m1_tf_file_info = file_mgr.borrow().get_file_info(&module1_test_file).unwrap(); - let Some(m1_tf_file_symbol) = SyncOdoo::get_symbol_of_opened_file( - &mut session, - &PathBuf::from(&module1_test_file) - ) else { - panic!("Failed to get file symbol"); - }; - - // Test references for BaseTestModel class - // BaseTestModel is referenced at: - // - Line 34 (0-indexed: 33): BaseOtherName = BaseTestModel - // - Line 35 (0-indexed: 34): baseInstance1 = BaseTestModel() - // - Line 37 (0-indexed: 36): ref_funcBase1 = BaseTestModel.get_test_int - let base_test_model_refs = test_utils::get_reference_locs(&mut session, &m1_tf_file_symbol, &m1_tf_file_info, 33, 17); - assert!(base_test_model_refs.len() >= 3, "Expected at least 3 references for BaseTestModel, got {}", base_test_model_refs.len()); - - // all refs should be in the same file - for loc in &base_test_model_refs { - assert_eq!(loc.uri.to_file_path().unwrap().sanitize(), module1_test_file, "Expected all references to be in the same file"); - } - - // Test references for 'var' variable in for_func method - // var is defined on line 21 (0-indexed: 20) and used on line 22 (0-indexed: 21) - let var_refs = test_utils::get_reference_locs(&mut session, &m1_tf_file_symbol, &m1_tf_file_info, 21, 18); - assert!(var_refs.len() >= 1, "Expected at least 1 reference for 'var', got {}", var_refs.len()); - - // Test references for CONSTANT_1 - // CONSTANT_1 is imported on line 2 (0-indexed: 1) and used on line 18 (0-indexed: 17) - // " return CONSTANT_1" - CONSTANT_1 starts at character 15 - let constant_refs = test_utils::get_reference_locs(&mut session, &m1_tf_file_symbol, &m1_tf_file_info, 17, 15); - assert!(constant_refs.len() >= 1, "Expected at least 1 reference for CONSTANT_1, got {}", constant_refs.len()); - - // Test references for 'self' parameter - // self is used multiple times within methods - let self_refs = test_utils::get_reference_locs(&mut session, &m1_tf_file_symbol, &m1_tf_file_info, 13, 8); - // self should have multiple references within the get_test_int method (lines 14, 15) - assert!(self_refs.len() >= 2, "Expected at least 2 references for 'self' in get_test_int method, got {}", self_refs.len()); -} +} \ No newline at end of file diff --git a/server/tests/test_references.rs b/server/tests/test_references.rs new file mode 100644 index 00000000..cbc3ef3e --- /dev/null +++ b/server/tests/test_references.rs @@ -0,0 +1,88 @@ +use std::path::PathBuf; + +use lsp_types::{Location, PartialResultParams, Position, TextDocumentContentChangeEvent, WorkDoneProgressParams}; +use odoo_ls_server::{core::file_mgr::FileMgr, features::references, threads::SessionInfo, utils::PathSanitizer}; +use tracing::error; + +use crate::setup::setup::{create_init_session, setup_server}; + +mod setup; + +#[test] +/// Test various calls to GotoReferences +fn test_references() { + // Setup server and session + let (mut odoo, config) = setup_server(true); + let mut session = create_init_session(&mut odoo, config); + let test_addons_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests").join("data").join("addons"); + + let test_file = test_addons_path.join("module_1").join("models").join("base_test_models.py").sanitize(); + //1. reference of a model + let references = get_references(&mut session, &test_file, Position::new(4, 29)); + for r in references.iter() { + error!("Reference found at {}:{}:{}", r.uri.as_str(), r.range.start.line, r.range.start.character); + } + assert!(references.len() >= 10, "Expected at least 10 references, got {}", references.len()); + for i in 0..9 { + assert!(references[i].uri.as_str().ends_with("base_test_models.py"), "Expected reference in base_test_models.py, got {}", references[i].uri.as_str()); + } + assert!(references[0].range.start.line == 4 && references[0].range.start.character == 12, "Expected 1th reference at line 4, character 12, got line {}, character {}", references[0].range.start.line, references[0].range.start.character); + assert!(references[1].range.start.line == 15 && references[1].range.start.character == 8, "Expected 2nd reference at line 15, character 8, got line {}, character {}", references[1].range.start.line, references[1].range.start.character); + assert!(references[2].range.start.line == 32 && references[2].range.start.character == 17, "Expected 3th reference at line 32, character 17, got line {}, character {}", references[2].range.start.line, references[2].range.start.character); + assert!(references[3].range.start.line == 33 && references[3].range.start.character == 8, "Expected 4th reference at line 33, character 8, got line {}, character {}", references[3].range.start.line, references[3].range.start.character); + assert!(references[4].range.start.line == 34 && references[4].range.start.character == 18, "Expected 5th reference at line 34, character 18, got line {}, character {}", references[4].range.start.line, references[4].range.start.character); + assert!(references[5].range.start.line == 40 && references[5].range.start.character == 16, "Expected 6th reference at line 40, character 16, got line {}, character {}", references[5].range.start.line, references[5].range.start.character); + assert!(references[6].range.start.line == 41 && references[6].range.start.character == 16, "Expected 7th reference at line 41, character 16, got line {}, character {}", references[6].range.start.line, references[6].range.start.character); + assert!(references[7].range.start.line == 42 && references[7].range.start.character == 16, "Expected 8th reference at line 42, character 16, got line {}, character {}", references[7].range.start.line, references[7].range.start.character); + assert!(references[8].range.start.line == 43 && references[8].range.start.character == 16, "Expected 9th reference at line 43, character 16, got line {}, character {}", references[8].range.start.line, references[8].range.start.character); + assert!(references[9].uri.as_str().ends_with("module_2/models/base_test_models.py"), "Expected reference 10 to be in module2/base_test_models.py, got {}", references[9].uri.as_str()); + assert!(references[10].range.start.line == 44 && references[10].range.start.character == 16, "Expected ninth reference at line 44, character 16, got line {}, character {}", references[10].range.start.line, references[10].range.start.character); + assert!(references.len() == 11, "Expected only 10 references, got unexpected additional references: {}", + references.iter().skip(11).map(|r| format!("{}:{}:{}", r.uri.as_str(), r.range.start.line, r.range.start.character)).collect::>().join(", ") + ); + + // reference of an attribute + let references = get_references(&mut session, &test_file, Position::new(9, 8)); + for r in references.iter() { + error!("Reference found at {}:{}:{}", r.uri.as_str(), r.range.start.line, r.range.start.character); + } + assert!(references.len() >= 8, "Expected at least 8 references, got {}", references.len()); + for i in 0..8 { + assert!(references[i].uri.as_str().ends_with("base_test_models.py"), "Expected reference in base_test_models.py, got {}", references[i].uri.as_str()); + } + assert!(references[0].range.start.line == 15 && references[0].range.start.character == 8, "Expected first reference at line 15, character 8, got line {}, character {}", references[0].range.start.line, references[0].range.start.character); + assert!(references[1].range.start.line == 32 && references[1].range.start.character == 8, "Expected second reference at line 32, character 8, got line {}, character {}", references[1].range.start.line, references[1].range.start.character); + assert!(references[2].range.start.line == 33 && references[2].range.start.character == 8, "Expected third reference at line 33, character 8, got line {}, character {}", references[2].range.start.line, references[2].range.start.character); + assert!(references[3].range.start.line == 34 && references[3].range.start.character == 18, "Expected fourth reference at line 34, character 18, got line {}, character {}", references[3].range.start.line, references[3].range.start.character); + assert!(references[4].range.start.line == 40 && references[4].range.start.character == 16, "Expected fifth reference at line 40, character 16, got line {}, character {}", references[4].range.start.line, references[4].range.start.character); + assert!(references[5].range.start.line == 41 && references[5].range.start.character == 16, "Expected sixth reference at line 41, character 16, got line {}, character {}", references[5].range.start.line, references[5].range.start.character); + assert!(references[6].range.start.line == 42 && references[6].range.start.character == 16, "Expected seventh reference at line 42, character 16, got line {}, character {}", references[6].range.start.line, references[6].range.start.character); + assert!(references[7].range.start.line == 43 && references[7].range.start.character == 16, "Expected eighth reference at line 43, character 16, got line {}, character {}", references[7].range.start.line, references[7].range.start.character); + assert!(references.len() == 8, "Expected only 8 references, got unexpected additional references: {}", + references.iter().skip(8).map(|r| format!("{}:{}:{}", r.uri.as_str(), r.range.start.line, r.range.start.character)).collect::>().join(", ") + ); + + //reference of a simple variable +} + +fn get_references(session: &mut SessionInfo, path: &String, position: Position)-> Vec { + let test_file_uri = FileMgr::pathname2uri(&path); + let references_params = lsp_types::ReferenceParams { + text_document_position: lsp_types::TextDocumentPositionParams { + text_document: lsp_types::TextDocumentIdentifier { + uri: test_file_uri, + }, + position, + }, + context: lsp_types::ReferenceContext { + include_declaration: true, + }, + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: PartialResultParams::default(), + }; + let references = odoo_ls_server::core::odoo::Odoo::handle_references(session, references_params); + assert!(references.is_ok(), "Expected Some result from handle_references, got Err"); + let references = references.unwrap(); + assert!(references.is_some(), "Expected Some result from handle_references, got None for {}:{}-{}", path, position.line, position.character); + references.unwrap() +} diff --git a/server/tests/test_utils.rs b/server/tests/test_utils.rs index d19beb74..fc25bc4b 100644 --- a/server/tests/test_utils.rs +++ b/server/tests/test_utils.rs @@ -170,14 +170,3 @@ pub fn get_resolved_symbols_at_position( .collect() } -/// Helper to get reference locations at a given (line, character) -pub fn get_reference_locs(session: &mut SessionInfo, f_sym: &Rc>, f_info: &Rc>, line: u32, character: u32) -> Vec { - let locations = odoo_ls_server::features::references::ReferenceFeature::get_references( - session, - f_sym, - f_info, - line, - character, - ); - locations.unwrap_or_default() -} From b937cb6158fa976d1da0dd66bc05771c3c3136e0 Mon Sep 17 00:00:00 2001 From: fda-odoo Date: Tue, 24 Feb 2026 11:36:51 +0100 Subject: [PATCH 9/9] [FIX] fix duplicate references and order in tests --- server/src/core/evaluation.rs | 5 +- server/src/features/references.rs | 19 ++-- .../module_1/models/base_test_models.py | 4 +- .../data/addons/module_1/models/models.py | 3 + server/tests/test_references.rs | 91 ++++++++++--------- 5 files changed, 63 insertions(+), 59 deletions(-) diff --git a/server/src/core/evaluation.rs b/server/src/core/evaluation.rs index 6c73df06..3e41210e 100644 --- a/server/src/core/evaluation.rs +++ b/server/src/core/evaluation.rs @@ -3,7 +3,6 @@ use itertools::FoldWhile::{Continue, Done}; use ruff_python_ast::{Arguments, Expr, ExprCall, Identifier, Number, Operator, Parameter, UnaryOp}; use ruff_text_size::{Ranged, TextRange, TextSize}; use lsp_types::{Diagnostic, Location, Position, Range}; -use tracing::error; use weak_table::traits::WeakElement; use std::cmp::{max, min}; use std::collections::{HashMap, HashSet}; @@ -12,7 +11,7 @@ use std::rc::{Rc, Weak}; use std::cell::RefCell; use crate::core::diagnostics::{create_diagnostic, DiagnosticCode}; use crate::utils::NoHashBuilder; -use crate::{constants::*, Sy}; +use crate::{Sy, constants::*, oyarn}; use crate::core::odoo::SyncOdoo; use crate::threads::SessionInfo; use crate::S; @@ -1394,7 +1393,7 @@ impl Evaluation { let class_bw = evaluation_search.borrow(); let class = class_bw.as_class_sym(); if let Some(model_data) = class._model.as_ref() { - if s.value.to_str() == model_data.name { + if oyarn!("{}", s.value.to_str()) == model_data.name { let file = parent.borrow().get_file().unwrap().upgrade().unwrap(); let file_info = session.sync_odoo.get_file_mgr().borrow().get_file_info(&file.borrow().paths()[0]); if let Some(file_info) = file_info { diff --git a/server/src/features/references.rs b/server/src/features/references.rs index 4ff4c5b8..7eb162c7 100644 --- a/server/src/features/references.rs +++ b/server/src/features/references.rs @@ -4,7 +4,6 @@ use std::{cell::RefCell, path::PathBuf, rc::Rc}; use lsp_types::{Location, Range}; use ruff_python_ast::{Alias, Expr, Identifier, Stmt, StmtAnnAssign, StmtAssert, StmtAssign, StmtAugAssign, StmtClassDef, StmtIf, StmtMatch, StmtRaise, StmtReturn, StmtTry, StmtTypeAlias, StmtWith}; use ruff_text_size::{Ranged, TextRange, TextSize}; -use tracing::error; use weak_table::PtrWeakHashSet; use crate::S; @@ -34,9 +33,7 @@ impl ReferenceFeature { match &definition.source { GotoSourceType::Symbol(target_symbol) => { let mut to_check: PtrWeakHashSet>> = PtrWeakHashSet::new(); - let symbol_name = target_symbol.borrow().name().to_string(); - locations.extend(ReferenceFeature::references_in_file(session, file_symbol, file_info, &symbol_name, &target_symbol)); to_check.insert(file_symbol.clone()); //take arch and arch_eval dependents @@ -60,14 +57,18 @@ impl ReferenceFeature { } } } + let mut files_to_check: PtrWeakHashSet>> = PtrWeakHashSet::new(); //only iter on files for sym in to_check.iter() { let Some(Some(file)) = sym.borrow().get_file().map(|x| x.upgrade()) else { //to be sure we are on a file continue; }; + files_to_check.insert(file.clone()); + } + for file in files_to_check.iter() { let Some(dep_file_info) = session.sync_odoo.get_file_mgr().borrow().get_file_info(&file.borrow().paths()[0]) else { continue; }; - locations.extend(ReferenceFeature::references_in_file(session, &sym, &dep_file_info, &symbol_name, &target_symbol)); + locations.extend(ReferenceFeature::references_in_file(session, &file, &dep_file_info, &target_symbol)); } }, _ => continue, @@ -81,7 +82,7 @@ impl ReferenceFeature { } } - fn references_in_file(session: &mut SessionInfo, file_symbol: &Rc>, file_info: &Rc>, symbol_name: &String, target_symbol_rc: &Rc>) -> Vec { + fn references_in_file(session: &mut SessionInfo, file_symbol: &Rc>, file_info: &Rc>, target_symbol_rc: &Rc>) -> Vec { let file_info_ast = file_info.borrow().file_info_ast.clone(); let mut visitor = ReferenceVisitor { sym_stack: vec![], @@ -333,14 +334,6 @@ impl ReferenceVisitor { } fn visit_assign(&mut self, session: &mut SessionInfo, assign: &StmtAssign) { - match *assign.value { - Expr::StringLiteral(ref s) => { - if s.value.to_str() == "pygls.tests.base_test_model" { - error!("here"); - } - }, - _=> {} - } self.visit_expr(session, &assign.value, &assign.range.start()); } diff --git a/server/tests/data/addons/module_1/models/base_test_models.py b/server/tests/data/addons/module_1/models/base_test_models.py index 02d7c8fb..7a8a8777 100644 --- a/server/tests/data/addons/module_1/models/base_test_models.py +++ b/server/tests/data/addons/module_1/models/base_test_models.py @@ -46,4 +46,6 @@ def _get_partner_id(self): return_funcBase2 = baseInstance2.get_test_int() class NoBaseModel(models.Model): - _inherit = "module_1.no_base_model" \ No newline at end of file + _inherit = "module_1.no_base_model" + +basic_var = 42 \ No newline at end of file diff --git a/server/tests/data/addons/module_1/models/models.py b/server/tests/data/addons/module_1/models/models.py index c8a9625f..482aa9ef 100644 --- a/server/tests/data/addons/module_1/models/models.py +++ b/server/tests/data/addons/module_1/models/models.py @@ -1,4 +1,7 @@ from odoo import models, fields +from .base_test_models import basic_var + +print(basic_var) class model_model(models.Model): pass diff --git a/server/tests/test_references.rs b/server/tests/test_references.rs index cbc3ef3e..598db5b2 100644 --- a/server/tests/test_references.rs +++ b/server/tests/test_references.rs @@ -1,8 +1,7 @@ use std::path::PathBuf; -use lsp_types::{Location, PartialResultParams, Position, TextDocumentContentChangeEvent, WorkDoneProgressParams}; -use odoo_ls_server::{core::file_mgr::FileMgr, features::references, threads::SessionInfo, utils::PathSanitizer}; -use tracing::error; +use lsp_types::{Location, PartialResultParams, Position, WorkDoneProgressParams}; +use odoo_ls_server::{core::file_mgr::FileMgr, threads::SessionInfo, utils::PathSanitizer}; use crate::setup::setup::{create_init_session, setup_server}; @@ -18,51 +17,59 @@ fn test_references() { let test_file = test_addons_path.join("module_1").join("models").join("base_test_models.py").sanitize(); //1. reference of a model - let references = get_references(&mut session, &test_file, Position::new(4, 29)); - for r in references.iter() { - error!("Reference found at {}:{}:{}", r.uri.as_str(), r.range.start.line, r.range.start.character); - } - assert!(references.len() >= 10, "Expected at least 10 references, got {}", references.len()); - for i in 0..9 { - assert!(references[i].uri.as_str().ends_with("base_test_models.py"), "Expected reference in base_test_models.py, got {}", references[i].uri.as_str()); - } - assert!(references[0].range.start.line == 4 && references[0].range.start.character == 12, "Expected 1th reference at line 4, character 12, got line {}, character {}", references[0].range.start.line, references[0].range.start.character); - assert!(references[1].range.start.line == 15 && references[1].range.start.character == 8, "Expected 2nd reference at line 15, character 8, got line {}, character {}", references[1].range.start.line, references[1].range.start.character); - assert!(references[2].range.start.line == 32 && references[2].range.start.character == 17, "Expected 3th reference at line 32, character 17, got line {}, character {}", references[2].range.start.line, references[2].range.start.character); - assert!(references[3].range.start.line == 33 && references[3].range.start.character == 8, "Expected 4th reference at line 33, character 8, got line {}, character {}", references[3].range.start.line, references[3].range.start.character); - assert!(references[4].range.start.line == 34 && references[4].range.start.character == 18, "Expected 5th reference at line 34, character 18, got line {}, character {}", references[4].range.start.line, references[4].range.start.character); - assert!(references[5].range.start.line == 40 && references[5].range.start.character == 16, "Expected 6th reference at line 40, character 16, got line {}, character {}", references[5].range.start.line, references[5].range.start.character); - assert!(references[6].range.start.line == 41 && references[6].range.start.character == 16, "Expected 7th reference at line 41, character 16, got line {}, character {}", references[6].range.start.line, references[6].range.start.character); - assert!(references[7].range.start.line == 42 && references[7].range.start.character == 16, "Expected 8th reference at line 42, character 16, got line {}, character {}", references[7].range.start.line, references[7].range.start.character); - assert!(references[8].range.start.line == 43 && references[8].range.start.character == 16, "Expected 9th reference at line 43, character 16, got line {}, character {}", references[8].range.start.line, references[8].range.start.character); - assert!(references[9].uri.as_str().ends_with("module_2/models/base_test_models.py"), "Expected reference 10 to be in module2/base_test_models.py, got {}", references[9].uri.as_str()); - assert!(references[10].range.start.line == 44 && references[10].range.start.character == 16, "Expected ninth reference at line 44, character 16, got line {}, character {}", references[10].range.start.line, references[10].range.start.character); - assert!(references.len() == 11, "Expected only 10 references, got unexpected additional references: {}", - references.iter().skip(11).map(|r| format!("{}:{}:{}", r.uri.as_str(), r.range.start.line, r.range.start.character)).collect::>().join(", ") + let mut references = get_references(&mut session, &test_file, Position::new(4, 29)); + //as order is not guaranteed (usage of set), check if the expected reference is somewhere in the result list. + assert_in_result(&mut references, "module_1/models/base_test_models.py", 4, 12); + assert_in_result(&mut references, "module_1/models/base_test_models.py", 15, 8); + assert_in_result(&mut references, "module_1/models/base_test_models.py", 32, 17); + assert_in_result(&mut references, "module_1/models/base_test_models.py", 33, 8); + assert_in_result(&mut references, "module_1/models/base_test_models.py", 34, 18); + assert_in_result(&mut references, "module_1/models/base_test_models.py", 40, 16); + assert_in_result(&mut references, "module_1/models/base_test_models.py", 41, 16); + assert_in_result(&mut references, "module_1/models/base_test_models.py", 42, 16); + assert_in_result(&mut references, "module_1/models/base_test_models.py", 43, 16); + assert_in_result(&mut references, "module_1/models/diagnostics.py", 9, 34); + assert_in_result(&mut references, "module_1/models/diagnostics.py", 17, 34); + assert_in_result(&mut references, "module_1/models/diagnostics.py", 18, 34); + assert_in_result(&mut references, "module_1/models/diagnostics.py", 19, 34); + assert_in_result(&mut references, "module_1/models/diagnostics.py", 20, 34); + assert_in_result(&mut references, "module_1/models/diagnostics.py", 22, 34); + assert_in_result(&mut references, "module_2/models/base_test_models.py", 5, 15); + assert!(references.len() == 0, "Some references were not expected: {}", + references.iter().map(|r| format!("{}:{}:{}", r.uri.as_str(), r.range.start.line, r.range.start.character)).collect::>().join(", ") ); // reference of an attribute - let references = get_references(&mut session, &test_file, Position::new(9, 8)); - for r in references.iter() { - error!("Reference found at {}:{}:{}", r.uri.as_str(), r.range.start.line, r.range.start.character); - } - assert!(references.len() >= 8, "Expected at least 8 references, got {}", references.len()); - for i in 0..8 { - assert!(references[i].uri.as_str().ends_with("base_test_models.py"), "Expected reference in base_test_models.py, got {}", references[i].uri.as_str()); - } - assert!(references[0].range.start.line == 15 && references[0].range.start.character == 8, "Expected first reference at line 15, character 8, got line {}, character {}", references[0].range.start.line, references[0].range.start.character); - assert!(references[1].range.start.line == 32 && references[1].range.start.character == 8, "Expected second reference at line 32, character 8, got line {}, character {}", references[1].range.start.line, references[1].range.start.character); - assert!(references[2].range.start.line == 33 && references[2].range.start.character == 8, "Expected third reference at line 33, character 8, got line {}, character {}", references[2].range.start.line, references[2].range.start.character); - assert!(references[3].range.start.line == 34 && references[3].range.start.character == 18, "Expected fourth reference at line 34, character 18, got line {}, character {}", references[3].range.start.line, references[3].range.start.character); - assert!(references[4].range.start.line == 40 && references[4].range.start.character == 16, "Expected fifth reference at line 40, character 16, got line {}, character {}", references[4].range.start.line, references[4].range.start.character); - assert!(references[5].range.start.line == 41 && references[5].range.start.character == 16, "Expected sixth reference at line 41, character 16, got line {}, character {}", references[5].range.start.line, references[5].range.start.character); - assert!(references[6].range.start.line == 42 && references[6].range.start.character == 16, "Expected seventh reference at line 42, character 16, got line {}, character {}", references[6].range.start.line, references[6].range.start.character); - assert!(references[7].range.start.line == 43 && references[7].range.start.character == 16, "Expected eighth reference at line 43, character 16, got line {}, character {}", references[7].range.start.line, references[7].range.start.character); - assert!(references.len() == 8, "Expected only 8 references, got unexpected additional references: {}", - references.iter().skip(8).map(|r| format!("{}:{}:{}", r.uri.as_str(), r.range.start.line, r.range.start.character)).collect::>().join(", ") + let mut references = get_references(&mut session, &test_file, Position::new(9, 8)); + assert_in_result(&mut references, "module_1/models/base_test_models.py", 37, 18); + assert!(references.len() == 0, "Some references were not expected: {}", + references.iter().map(|r| format!("{}:{}:{}", r.uri.as_str(), r.range.start.line, r.range.start.character)).collect::>().join(", ") ); //reference of a simple variable + let mut references = get_references(&mut session, &test_file, Position::new(50, 4)); + assert_in_result(&mut references, "module_1/models/models.py", 1, 30); + assert!(references.len() == 0, "Some references were not expected: {}", + references.iter().map(|r| format!("{}:{}:{}", r.uri.as_str(), r.range.start.line, r.range.start.character)).collect::>().join(", ") + ); + + // for r in references.iter() { + // error!("Reference found at {}:{}:{}", r.uri.as_str(), r.range.start.line, r.range.start.character); + // } +} + +fn assert_in_result(references: &mut Vec, end_path: &str, line: u32, character: u32) { + let mut index = None; + for (i, r) in references.iter().enumerate() { + if r.uri.as_str().ends_with(end_path) && r.range.start.line == line && r.range.start.character == character { + index = Some(i); + } + } + if let Some(i) = index { + references.remove(i); + } else { + assert!(false, "Expected reference not found: {}:{}:{}", end_path, line, character); + } } fn get_references(session: &mut SessionInfo, path: &String, position: Position)-> Vec {