diff --git a/server/src/core/diagnostic_codes_list.rs b/server/src/core/diagnostic_codes_list.rs index c0cc5b12..b6ffb55b 100644 --- a/server/src/core/diagnostic_codes_list.rs +++ b/server/src/core/diagnostic_codes_list.rs @@ -155,6 +155,18 @@ OLS03019, DiagnosticSetting::Error, "Compute method not set to modify this field * Hence, this model is shadowing an existing model. */ OLS03020, DiagnosticSetting::Warning, "Model {0} is shadowing an existing model in dependencies", +/** + * On a One2many field, the inverse_name should be a field on the comodel that is a Many2one to the current model. + */ +OLS03021, DiagnosticSetting::Error, "Inverse field {0} does not exist on comodel {1}", +/** + * On a One2many field, the inverse_name should be a field on the comodel that is a Many2one to the current model. + */ +OLS03022, DiagnosticSetting::Error, "Inverse field is not a Many2one field", +/** + * On a One2many field, the inverse_name should be a field on the comodel that is a Many2one to the current model. + */ +OLS03023, DiagnosticSetting::Error, "Inverse field {0} is not pointing to the current model {1}, but rather to {2}", /** * A __manifest__.py file should be evaluated with a literal_eval to a single dictionary. * Do not store any other information in it. diff --git a/server/src/core/evaluation.rs b/server/src/core/evaluation.rs index 653d44fd..c5930e55 100644 --- a/server/src/core/evaluation.rs +++ b/server/src/core/evaluation.rs @@ -894,7 +894,7 @@ impl Evaluation { } } //1: find __init__ method - let init = base_sym.borrow().get_member_symbol(session, &S!("__init__"), module.clone(), true, false, false, false); + let init = base_sym.borrow().get_member_symbol(session, &S!("__init__"), module.clone(), true, false, false, false, false); let mut found_hook = false; if let Some(init) = init.0.first() { if let Some(init_eval) = init.borrow().evaluations() { @@ -1039,7 +1039,7 @@ impl Evaluation { } } let is_super = ibase.is_weak() && ibase.as_weak().is_super; - let (attributes, mut attributes_diagnostics) = base_loc.borrow().get_member_symbol(session, &expr.attr.to_string(), module.clone(), false, false, true, is_super); + let (attributes, mut attributes_diagnostics) = base_loc.borrow().get_member_symbol(session, &expr.attr.to_string(), module.clone(), false, false, false, true, is_super); for diagnostic in attributes_diagnostics.iter_mut(){ diagnostic.range = FileMgr::textRange_to_temporary_Range(&expr.range()) } @@ -1265,7 +1265,7 @@ impl Evaluation { for base_eval_ptr in base_eval_ptrs.iter() { let EvaluationSymbolPtr::WEAK(base_sym_weak_eval) = base_eval_ptr else {continue}; let Some(base_sym) = base_sym_weak_eval.weak.upgrade() else {continue}; - let (operator_functions, diags) = base_sym.borrow().get_member_symbol(session, &S!(method), module.clone(), true, false, false, false); + let (operator_functions, diags) = base_sym.borrow().get_member_symbol(session, &S!(method), module.clone(), true, false, true, false, false); diagnostics.extend(diags); for operator_function in operator_functions.into_iter(){ for eval in operator_function.borrow().evaluations().unwrap_or(&vec![]).iter() { @@ -1569,6 +1569,7 @@ impl Evaluation { false, true, false, + false, false); if symbols.is_empty() { if let Some(diagnostic_base) = create_diagnostic(session, DiagnosticCode::OLS03011, &[&name, &object.borrow().name()]) { diff --git a/server/src/core/python_arch_eval.rs b/server/src/core/python_arch_eval.rs index abfeadbb..a9e8d71a 100644 --- a/server/src/core/python_arch_eval.rs +++ b/server/src/core/python_arch_eval.rs @@ -811,7 +811,7 @@ impl PythonArchEval { let symbol_type_rc = symbol_eval[0].upgrade_weak().unwrap(); let symbol_type = symbol_type_rc.borrow(); if symbol_type.typ() == SymType::CLASS { - let (iter, _) = symbol_type.get_member_symbol(session, &S!("__iter__"), None, true, false, false, false); + let (iter, _) = symbol_type.get_member_symbol(session, &S!("__iter__"), None, true, false, false, false, false); if iter.len() == 1 { SyncOdoo::build_now(session, &iter[0], BuildSteps::ARCH_EVAL); SyncOdoo::build_now(session, &iter[0], BuildSteps::VALIDATION); @@ -1030,6 +1030,7 @@ impl PythonArchEval { from_module.clone(), false, true, + false, true, false); if ix == split_expr.len() - 1 { diff --git a/server/src/core/python_arch_eval_hooks.rs b/server/src/core/python_arch_eval_hooks.rs index 7e7404dd..761d5a3c 100644 --- a/server/src/core/python_arch_eval_hooks.rs +++ b/server/src/core/python_arch_eval_hooks.rs @@ -1005,6 +1005,7 @@ impl PythonArchEvalHooks { ("comodel_name", "str"), ("related", "str"), ("compute", "str"), + ("inverse_name", "str"), ("delegate", "bool"), ("required", "bool"), ("default", "bool"), @@ -1125,7 +1126,7 @@ impl PythonArchEvalHooks { for arg in arguments.args.iter() { let Expr::StringLiteral(expr) = arg else {return diagnostics}; let field_name = expr.value.to_string(); - let (syms, _) = class_sym.borrow().get_member_symbol(session, &field_name, from_module.clone(), false, false, true, false); + let (syms, _) = class_sym.borrow().get_member_symbol(session, &field_name, from_module.clone(), false, true, false, true, false); if syms.is_empty(){ if let Some(diagnostic) = create_diagnostic(session, DiagnosticCode::OLS03014, &[&field_name, &model_name]) { diagnostics.push(Diagnostic { diff --git a/server/src/core/python_odoo_builder.rs b/server/src/core/python_odoo_builder.rs index 49357f44..8bffa531 100644 --- a/server/src/core/python_odoo_builder.rs +++ b/server/src/core/python_odoo_builder.rs @@ -194,7 +194,7 @@ impl PythonOdooBuilder { } fn _get_attribute(session: &mut SessionInfo, loc_sym: &mut Symbol, attr: &String, diagnostics: &mut Vec) -> Option { - let (attr_sym, _) = loc_sym.get_member_symbol(session, attr, None, true, false, false, false); + let (attr_sym, _) = loc_sym.get_member_symbol(session, attr, None, true, false, false, false, false); if attr_sym.len() == 0 { return None; } diff --git a/server/src/core/python_validator.rs b/server/src/core/python_validator.rs index d04d3792..a99aaf9e 100644 --- a/server/src/core/python_validator.rs +++ b/server/src/core/python_validator.rs @@ -7,7 +7,7 @@ use std::path::PathBuf; use lsp_types::{Diagnostic, Position, Range}; use crate::core::diagnostics::{create_diagnostic, DiagnosticCode}; use crate::core::evaluation::ContextValue; -use crate::{constants::*, Sy}; +use crate::{constants::*, oyarn, Sy}; use crate::core::symbols::symbol::Symbol; use crate::core::odoo::SyncOdoo; use crate::core::symbols::module_symbol::ModuleSymbol; @@ -422,7 +422,7 @@ impl PythonValidator { } let Some(field_type) = symbol .borrow() - .get_member_symbol(session, &S!("type"), None, false, false, false, false) + .get_member_symbol(session, &S!("type"), None, false, false, false, false, false) .0.first() .and_then(|field_type_var| field_type_var.borrow().evaluations().cloned()) .and_then(|evals| evals.first().cloned()) @@ -445,7 +445,7 @@ impl PythonValidator { }; let found = related_field_class_sym .borrow() - .get_member_symbol(session, &S!("type"), None, false, false, false, false) + .get_member_symbol(session, &S!("type"), None, false, false, false, false, false) .0.first() .and_then(|field_type_var| field_type_var.borrow().evaluations().cloned()) .and_then(|evals| evals.first().cloned()) @@ -507,9 +507,10 @@ impl PythonValidator { false, false, true, + true, false ); - let method_found = symbols.iter().any(|symbol| symbol.borrow().typ() == SymType::FUNCTION); + let method_found = !symbols.is_empty(); if !method_found{ let Some(arg_range) = eval_weak.as_weak().context.get(&format!("{special_fn_field_name}_arg_range")).map(|ctx_val| ctx_val.as_text_range()) else { continue; @@ -523,6 +524,80 @@ impl PythonValidator { } } + if let Some(inverse_name) = eval_weak.as_weak().context.get(&S!("inverse_name")).map(|ctx_val| ctx_val.as_string()) { + let Some(model_name) = eval_weak.as_weak().context.get(&S!("comodel_name")).map(|ctx_val| ctx_val.as_string()) else { + continue; + }; + let Some(model) = session.sync_odoo.models.get(&oyarn!("{}", model_name)).cloned() else { + continue; + }; + let Some(module) = class_ref.find_module() else { + continue; + }; + let main_syms = model.borrow().get_main_symbols(session, Some(module.clone())); + let symbols: Vec<_> = main_syms.iter().flat_map(|main_sym| + main_sym.clone().borrow().get_member_symbol(session, &inverse_name, Some(module.clone()), false, true, false, true, false).0 + ).collect(); + let method_found = !symbols.is_empty(); + if !method_found{ + let Some(arg_range) = eval_weak.as_weak().context.get(&format!("inverse_name_arg_range")).map(|ctx_val| ctx_val.as_text_range()) else { + continue; + }; + if let Some(diagnostic_base) = create_diagnostic(&session, DiagnosticCode::OLS03021, &[&inverse_name, &model_name]) { + self.diagnostics.push(Diagnostic { + range: Range::new(Position::new(arg_range.start().to_u32(), 0), Position::new(arg_range.end().to_u32(), 0)), + ..diagnostic_base.clone() + }); + } + } + if symbols.iter().any(|sym| !sym.borrow().is_specific_field(session, &["Many2one"])) { + let Some(arg_range) = eval_weak.as_weak().context.get(&format!("inverse_name_arg_range")).map(|ctx_val| ctx_val.as_text_range()) else { + continue; + }; + if let Some(diagnostic_base) = create_diagnostic(&session, DiagnosticCode::OLS03022, &[]) { + self.diagnostics.push(Diagnostic { + range: Range::new(Position::new(arg_range.start().to_u32(), 0), Position::new(arg_range.end().to_u32(), 0)), + ..diagnostic_base.clone() + }); + } + } else { + // Check if we have a many2one field pointing to the comodel with another name than the current model + let mut comodel_eval_weaks = Vec::new(); + for sym in symbols.iter() { + let sym_ref = sym.borrow(); + let evals = sym_ref.evaluations().as_ref().unwrap().iter(); + for eval in evals { + let followed = Symbol::follow_ref( + &eval.symbol.get_symbol(session, &mut None, &mut vec![], None), + session, + &mut None, + true, + false, + None, + ); + comodel_eval_weaks.extend(followed); + } + } + for comodel_eval_weak in comodel_eval_weaks { + let Some(model_name) = comodel_eval_weak.as_weak().context.get(&S!("comodel_name")).map(|ctx_val| ctx_val.as_string()) else { + continue; + }; + if model_name == model_data.name { // valid + continue; + } + let Some(arg_range) = eval_weak.as_weak().context.get(&format!("inverse_name_arg_range")).map(|ctx_val| ctx_val.as_text_range()) else { + continue; + }; + if let Some(diagnostic_base) = create_diagnostic(&session, DiagnosticCode::OLS03023, &[&inverse_name, &model_data.name, &model_name]) { + self.diagnostics.push(Diagnostic { + range: Range::new(Position::new(arg_range.start().to_u32(), 0), Position::new(arg_range.end().to_u32(), 0)), + ..diagnostic_base.clone() + }); + break; + } + } + } + } } } } diff --git a/server/src/core/symbols/symbol.rs b/server/src/core/symbols/symbol.rs index 27953c11..5545bd9e 100644 --- a/server/src/core/symbols/symbol.rs +++ b/server/src/core/symbols/symbol.rs @@ -2123,7 +2123,7 @@ impl Symbol { if let Some(base_attr) = base_attr { let attribute_type_sym = symbol; //TODO shouldn't we set the from_module in the call to get_member_symbol? - let get_method = attribute_type_sym.get_member_symbol(session, &S!("__get__"), None, true, false, true, false).0.first().cloned(); + let get_method = attribute_type_sym.get_member_symbol(session, &S!("__get__"), None, true, false, false, true, false).0.first().cloned(); match get_method { Some(get_method) if (base_attr.borrow().typ() == SymType::CLASS) => { let get_method = get_method.borrow(); @@ -2746,12 +2746,33 @@ impl Symbol { if not all, it will return the first found. If all, the all found symbols are returned, but the first one is the one that is overriding others. :param: from_module: optional, can change the from_module of the given class */ - pub fn get_member_symbol(&self, session: &mut SessionInfo, name: &String, from_module: Option>>, prevent_comodel: bool, only_fields: bool, all: bool, is_super: bool) -> (Vec>>, Vec) { + pub fn get_member_symbol( + &self, + session: &mut SessionInfo, + name: &String, + from_module: Option>>, + prevent_comodel: bool, + only_fields: bool, + only_methods: bool, + all: bool, + is_super: bool + ) -> (Vec>>, Vec) { let mut visited_classes: PtrWeakHashSet>> = PtrWeakHashSet::new(); - return self._get_member_symbol_helper(session, name, from_module, prevent_comodel, only_fields, all, is_super, &mut visited_classes); + return self._get_member_symbol_helper(session, name, from_module, prevent_comodel, only_fields, only_methods, all, is_super, &mut visited_classes); } - fn _get_member_symbol_helper(&self, session: &mut SessionInfo, name: &String, from_module: Option>>, prevent_comodel: bool, only_fields: bool, all: bool, is_super: bool, visited_classes: &mut PtrWeakHashSet>>) -> (Vec>>, Vec) { + fn _get_member_symbol_helper( + &self, + session: &mut SessionInfo, + name: &String, + from_module: Option>>, + prevent_comodel: bool, + only_fields: bool, + only_methods: bool, + all: bool, + is_super: bool, + visited_classes: &mut PtrWeakHashSet>> + ) -> (Vec>>, Vec) { let mut result: Vec>> = vec![]; let mut visited_symbols: PtrWeakHashSet>> = PtrWeakHashSet::new(); let mut extend_result = |syms: Vec>>| { @@ -2779,6 +2800,9 @@ impl Symbol { if only_fields { content_syms = content_syms.iter().filter(|x| x.borrow().is_field(session)).cloned().collect(); } + if only_methods { + content_syms = content_syms.iter().filter(|x| x.borrow().typ() == SymType::FUNCTION).cloned().collect(); + } if !content_syms.is_empty() { if all { extend_result(content_syms); @@ -2801,7 +2825,7 @@ impl Symbol { continue; } visited_classes.insert(model_symbol.clone()); - let (attributs, att_diagnostic) = model_symbol.borrow()._get_member_symbol_helper(session, name, None, true, only_fields, all, false, visited_classes); + let (attributs, att_diagnostic) = model_symbol.borrow()._get_member_symbol_helper(session, name, None, true, only_fields, only_methods, all, false, visited_classes); diagnostics.extend(att_diagnostic); if all { extend_result(attributs); @@ -2819,7 +2843,7 @@ impl Symbol { continue; } visited_classes.insert(model_symbol.clone()); - let (attributs, att_diagnostic) = model_symbol.borrow()._get_member_symbol_helper(session, name, None, true, true, all, false, visited_classes); + let (attributs, att_diagnostic) = model_symbol.borrow()._get_member_symbol_helper(session, name, None, true, true, only_methods, all, false, visited_classes); diagnostics.extend(att_diagnostic); if all { extend_result(attributs); @@ -2843,7 +2867,7 @@ impl Symbol { continue; } visited_classes.insert(base.clone()); - let (s, s_diagnostic) = base.borrow().get_member_symbol(session, name, from_module.clone(), prevent_comodel, only_fields, all, false); + let (s, s_diagnostic) = base.borrow().get_member_symbol(session, name, from_module.clone(), prevent_comodel, only_fields, only_methods, all, false); diagnostics.extend(s_diagnostic); if !s.is_empty() { if all { diff --git a/server/src/features/completion.rs b/server/src/features/completion.rs index 23649e38..06bc88f0 100644 --- a/server/src/features/completion.rs +++ b/server/src/features/completion.rs @@ -33,6 +33,7 @@ pub enum ExpectedType { CLASS(Rc>), SIMPLE_FIELD(Option), NESTED_FIELD(Option), + EXTERNAL_FIELD(OYarn), // Like in inverse_name='field_name', we attach the comodel_name METHOD_NAME, INHERITS, } @@ -635,35 +636,44 @@ fn complete_call(session: &mut SessionInfo, file: &Rc>, expr_cal return complete_expr(arg, session, file, offset, is_param, &vec![]); } } - for keyword in expr_call.arguments.keywords.iter(){ - if offset <= keyword.value.range().start().to_usize() || offset > keyword.value.range().end().to_usize() { + let Some(keyword) = expr_call.arguments.keywords.iter().find(|arg| + offset > arg.range().start().to_usize() && offset <= arg.range().end().to_usize()) else { + return None; + }; + for callable_eval in callable_evals.iter() { + let callable = callable_eval.symbol.get_symbol_as_weak(session, &mut None, &mut vec![], None); + let Some(callable_sym) = callable.weak.upgrade() else {continue}; + if callable_sym.borrow().typ() != SymType::CLASS || !callable_sym.borrow().is_field_class(session){ continue; } - for callable_eval in callable_evals.iter() { - let callable = callable_eval.symbol.get_symbol_as_weak(session, &mut None, &mut vec![], None); - let Some(callable_sym) = callable.weak.upgrade() else {continue}; - if callable_sym.borrow().typ() != SymType::CLASS || !callable_sym.borrow().is_field_class(session){ - continue; + let Some(expected_type) = keyword.arg.as_ref().and_then(|kw_arg_id| + match kw_arg_id.id.as_str() { + "related" => Some(vec![ExpectedType::NESTED_FIELD(Some(oyarn!("{}", callable_sym.borrow().name())))]), + "comodel_name" => if callable_sym.borrow().is_specific_field_class(session, &["Many2one", "One2many", "Many2many"]){ + Some(vec![ExpectedType::MODEL_NAME]) + } else { + None + }, + "inverse_name" => { + if let Some(Expr::StringLiteral(expr)) = expr_call.arguments.args.first() { + Some(vec![ExpectedType::EXTERNAL_FIELD(Sy!(expr.value.to_string()))]) + } else { + expr_call.arguments.keywords.iter().find(|kw| kw.arg.as_ref().map(|arg| arg.id == "comodel_name").unwrap_or(false)) + .and_then(|kw| match &kw.value { + Expr::StringLiteral(expr) => Some(vec![ExpectedType::EXTERNAL_FIELD(Sy!(expr.value.to_string()))]), + _ => None + }) + } + }, + "inverse" | "search" | "compute" => Some(vec![ExpectedType::METHOD_NAME]), + _ => None, } - let Some(expected_type) = keyword.arg.as_ref().and_then(|kw_arg_id| - match kw_arg_id.id.as_str() { - "related" => Some(vec![ExpectedType::NESTED_FIELD(Some(oyarn!("{}", callable_sym.borrow().name())))]), - "comodel_name" => if callable_sym.borrow().is_specific_field_class(session, &["Many2one", "One2many", "Many2many"]){ - Some(vec![ExpectedType::MODEL_NAME]) - } else { - None - }, - "inverse" | "search" | "compute" => Some(vec![ExpectedType::METHOD_NAME]), - _ => None, - } - ) else { - continue; - }; - return complete_expr(&keyword.value, session, file, offset, is_param, &expected_type); - } - return complete_expr(&keyword.value, session, file, offset, is_param, &vec![]); + ) else { + continue; + }; + return complete_expr(&keyword.value, session, file, offset, is_param, &expected_type); } - None + return complete_expr(&keyword.value, session, file, offset, is_param, &vec![]); } fn complete_string_literal(session: &mut SessionInfo, file: &Rc>, expr_string_literal: &ruff_python_ast::ExprStringLiteral, _offset: usize, _is_param: bool, expected_type: &Vec) -> Option { @@ -767,6 +777,15 @@ fn complete_string_literal(session: &mut SessionInfo, file: &Rc> _ => unreachable!() } }, + ExpectedType::EXTERNAL_FIELD(model_name) => { + let Some(model) = session.sync_odoo.models.get(&oyarn!("{}", model_name)).cloned() else { + break; + }; + let main_syms = model.borrow().get_main_symbols(session, current_module.clone()); + main_syms.iter().for_each(|model_sym| { + add_model_attributes(session, &mut items, current_module.clone(), model_sym.clone(), false, true, false, expr_string_literal.value.to_str(), &Some(S!("Many2one"))) + }); + }, ExpectedType::CLASS(_) => {}, ExpectedType::INHERITS => {}, } @@ -999,6 +1018,7 @@ fn add_nested_field_names( from_module.clone(), false, true, + false, true, false); if symbols.is_empty() { diff --git a/server/src/features/features_utils.rs b/server/src/features/features_utils.rs index 401fe935..54a3d84f 100644 --- a/server/src/features/features_utils.rs +++ b/server/src/features/features_utils.rs @@ -53,12 +53,15 @@ impl FeaturesUtils { call_expr: &ExprCall, offset: &usize, ) -> Vec>>{ - if let Some((_, keyword)) = call_expr.arguments.keywords.iter().enumerate().find(|(_, arg)| + if let Some(keyword) = call_expr.arguments.keywords.iter().find(|arg| *offset > arg.range().start().to_usize() && *offset <= arg.range().end().to_usize() ){ let Some(ref arg_id) = keyword.arg else { return vec![]; }; + if arg_id.as_str() == "inverse_name" { + return FeaturesUtils::find_inverse_name_field_symbol(session, from_module, field_value, call_expr); + } if !["compute", "inverse", "search"].contains(&arg_id.as_str()){ return vec![]; } @@ -81,7 +84,32 @@ impl FeaturesUtils { ) { return vec![]; } - parent_class.clone().borrow().get_member_symbol(session, field_value, from_module.clone(), false, false, true, false).0 + parent_class.clone().borrow().get_member_symbol(session, field_value, from_module.clone(), false, false, true, true, false).0 + } + + fn find_inverse_name_field_symbol( + session: &mut SessionInfo, + from_module: Option>>, + field_value: &String, + call_expr: &ExprCall, + ) -> Vec>>{ + let model_name = if let Some(Expr::StringLiteral(expr)) = call_expr.arguments.args.first() { + expr.value.to_string() + } else { + let Some(model_name) = call_expr.arguments.keywords.iter().find(|kw| kw.arg.as_ref().map(|arg| arg.id == "comodel_name").unwrap_or(false)) + .and_then(|kw| match &kw.value { + Expr::StringLiteral(expr) => Some(expr.value.to_string()), + _ => None + }) else { + return vec![]; + }; + model_name + }; + let Some(model) = session.sync_odoo.models.get(&oyarn!("{}", model_name)).cloned() else { + return vec![]; + }; + let main_syms = model.borrow().get_main_symbols(session, from_module.clone()); + main_syms.iter().flat_map(|main_sym| main_sym.clone().borrow().get_member_symbol(session, field_value, from_module.clone(), false, true, false, true, false).0).collect() } fn find_simple_decorator_field_symbol( @@ -96,7 +124,7 @@ impl FeaturesUtils { if parent_class.borrow().as_class_sym()._model.is_none(){ return vec![]; } - parent_class.clone().borrow().get_member_symbol(session, field_name, from_module.clone(), false, false, true, false).0 + parent_class.clone().borrow().get_member_symbol(session, field_name, from_module.clone(), false, true, false, true, false).0 } fn find_nested_fields( @@ -119,7 +147,7 @@ impl FeaturesUtils { let range_end = range_start + TextSize::new((name.len() + 1) as u32); let cursor_section = TextRange::new(range_start, range_end).contains(TextSize::new(*offset as u32)); if cursor_section { - let fields = parent_object.clone().unwrap().borrow().get_member_symbol(session, &name, from_module.clone(), false, true, true, false).0; + let fields = parent_object.clone().unwrap().borrow().get_member_symbol(session, &name, from_module.clone(), false, true, false,true, false).0; return fields.into_iter().map(|f| (f, TextRange::new(range_start, range_end - TextSize::new(1)))).collect(); } else { let (symbols, _diagnostics) = parent_object.clone().unwrap().borrow().get_member_symbol(session, @@ -127,6 +155,7 @@ impl FeaturesUtils { from_module.clone(), false, true, + false, true, false); if symbols.is_empty() {