Skip to content

Commit dee8e85

Browse files
committed
[IMP] Handle lambdas and other missing expressions
1 parent b937cb6 commit dee8e85

File tree

7 files changed

+523
-134
lines changed

7 files changed

+523
-134
lines changed

server/src/core/evaluation.rs

Lines changed: 243 additions & 48 deletions
Large diffs are not rendered by default.

server/src/core/python_arch_builder.rs

Lines changed: 73 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::cell::RefCell;
33
use std::vec;
44
use anyhow::Error;
55
use ruff_text_size::{Ranged, TextRange, TextSize};
6-
use ruff_python_ast::{Alias, AnyRootNodeRef, CmpOp, Expr, ExprNamed, ExprTuple, FStringPart, Identifier, Pattern, Stmt, StmtAnnAssign, StmtAssign, StmtClassDef, StmtFor, StmtFunctionDef, StmtIf, StmtMatch, StmtTry, StmtWhile, StmtWith};
6+
use ruff_python_ast::{Alias, AnyRootNodeRef, CmpOp, Expr, ExprNamed, ExprTuple, FStringPart, Identifier, Parameters, Pattern, Stmt, StmtAnnAssign, StmtAssign, StmtClassDef, StmtFor, StmtFunctionDef, StmtIf, StmtMatch, StmtTry, StmtWhile, StmtWith};
77
use lsp_types::Diagnostic;
88
use tracing::{trace, warn};
99
use weak_table::traits::WeakElement;
@@ -456,12 +456,16 @@ impl PythonArchBuilder {
456456
expr_slice.lower.as_ref().map(|lower_expr| self.visit_expr(session, &lower_expr));
457457
},
458458
// Expressions that cannot contained a named expressions are not traversed
459-
Expr::Lambda(_todo_lambda_expr) => {
460-
// Lambdas can have named expressions, but it is not a common use
461-
// Like lambda vals: vals[(x := 0): x + 3]
462-
// However x is only in scope in the lambda expression only
463-
// It needs adding a new function, ast_indexes, then add the variable inside
464-
// I deem it currently unnecessary
459+
Expr::Lambda(lambda_expr) => {
460+
let sym = self.sym_stack.last().unwrap().borrow_mut().add_new_function(
461+
session, &S!("<lambda>"), &lambda_expr.range, &lambda_expr.body.range().start()
462+
);
463+
if let Some(parameters) = &lambda_expr.parameters {
464+
PythonArchBuilder::handle_func_args(&sym, session, &parameters);
465+
}
466+
self.sym_stack.push(sym.clone());
467+
self.visit_expr(session, &lambda_expr.body);
468+
self.sym_stack.pop();
465469
},
466470
Expr::Generator(_todo_expr_generator) => {
467471
// generators are lazily evaluated,
@@ -661,6 +665,67 @@ impl PythonArchBuilder {
661665
}
662666
}
663667

668+
fn handle_func_args(fun_sym: &Rc<RefCell<Symbol>>, session: &mut SessionInfo, parameters: &Parameters) {
669+
for arg in parameters.posonlyargs.iter() {
670+
let param = fun_sym.borrow_mut().add_new_variable(session, oyarn!("{}", arg.parameter.name.id), &arg.range);
671+
param.borrow_mut().as_variable_mut().is_parameter = true;
672+
let mut default = None;
673+
if arg.default.is_some() {
674+
default = Some(Evaluation::new_none()); //TODO evaluate default? actually only used to know if there is a default or not
675+
}
676+
fun_sym.borrow_mut().as_func_mut().args.push(Argument {
677+
symbol: Rc::downgrade(&param),
678+
default_value: default,
679+
arg_type: ArgumentType::POS_ONLY,
680+
annotation: arg.parameter.annotation.clone(),
681+
});
682+
}
683+
for arg in parameters.args.iter() {
684+
let param = fun_sym.borrow_mut().add_new_variable(session, oyarn!("{}", arg.parameter.name.id), &arg.range);
685+
param.borrow_mut().as_variable_mut().is_parameter = true;
686+
let mut default = None;
687+
if arg.default.is_some() {
688+
default = Some(Evaluation::new_none()); //TODO evaluate default? actually only used to know if there is a default or not
689+
}
690+
fun_sym.borrow_mut().as_func_mut().args.push(Argument {
691+
symbol: Rc::downgrade(&param),
692+
default_value: default,
693+
arg_type: ArgumentType::ARG,
694+
annotation: arg.parameter.annotation.clone(),
695+
});
696+
}
697+
if let Some(arg) = &parameters.vararg {
698+
let param = fun_sym.borrow_mut().add_new_variable(session, oyarn!("{}", arg.name.id), &arg.range);
699+
param.borrow_mut().as_variable_mut().is_parameter = true;
700+
fun_sym.borrow_mut().as_func_mut().args.push(Argument {
701+
symbol: Rc::downgrade(&param),
702+
default_value: None,
703+
arg_type: ArgumentType::VARARG,
704+
annotation: arg.annotation.clone(),
705+
});
706+
}
707+
for arg in parameters.kwonlyargs.iter() {
708+
let param = fun_sym.borrow_mut().add_new_variable(session, oyarn!("{}", arg.parameter.name.id), &arg.range);
709+
param.borrow_mut().as_variable_mut().is_parameter = true;
710+
fun_sym.borrow_mut().as_func_mut().args.push(Argument {
711+
symbol: Rc::downgrade(&param),
712+
default_value: arg.default.as_ref().map(|_default| Evaluation::new_none()),
713+
arg_type: ArgumentType::KWORD_ONLY,
714+
annotation: arg.parameter.annotation.clone(),
715+
});
716+
}
717+
if let Some(arg) = &parameters.kwarg {
718+
let param = fun_sym.borrow_mut().add_new_variable(session, oyarn!("{}", arg.name.id), &arg.range);
719+
param.borrow_mut().as_variable_mut().is_parameter = true;
720+
fun_sym.borrow_mut().as_func_mut().args.push(Argument {
721+
symbol: Rc::downgrade(&param),
722+
default_value: None,
723+
arg_type: ArgumentType::KWARG,
724+
annotation: arg.annotation.clone(),
725+
});
726+
}
727+
}
728+
664729
fn visit_func_def(&mut self, session: &mut SessionInfo, func_def: &StmtFunctionDef) -> Result<(), Error> {
665730
if func_def.body.is_empty() {
666731
return Ok(()) //if body is empty, it usually means that the ast of the class is invalid. Skip it
@@ -706,64 +771,7 @@ impl PythonArchBuilder {
706771
}
707772
drop(sym_bw);
708773
//add params
709-
for arg in func_def.parameters.posonlyargs.iter() {
710-
let param = sym.borrow_mut().add_new_variable(session, oyarn!("{}", arg.parameter.name.id), &arg.range);
711-
param.borrow_mut().as_variable_mut().is_parameter = true;
712-
let mut default = None;
713-
if arg.default.is_some() {
714-
default = Some(Evaluation::new_none()); //TODO evaluate default? actually only used to know if there is a default or not
715-
}
716-
sym.borrow_mut().as_func_mut().args.push(Argument {
717-
symbol: Rc::downgrade(&param),
718-
default_value: default,
719-
arg_type: ArgumentType::POS_ONLY,
720-
annotation: arg.parameter.annotation.clone(),
721-
});
722-
}
723-
for arg in func_def.parameters.args.iter() {
724-
let param = sym.borrow_mut().add_new_variable(session, oyarn!("{}", arg.parameter.name.id), &arg.range);
725-
param.borrow_mut().as_variable_mut().is_parameter = true;
726-
let mut default = None;
727-
if arg.default.is_some() {
728-
default = Some(Evaluation::new_none()); //TODO evaluate default? actually only used to know if there is a default or not
729-
}
730-
sym.borrow_mut().as_func_mut().args.push(Argument {
731-
symbol: Rc::downgrade(&param),
732-
default_value: default,
733-
arg_type: ArgumentType::ARG,
734-
annotation: arg.parameter.annotation.clone(),
735-
});
736-
}
737-
if let Some(arg) = &func_def.parameters.vararg {
738-
let param = sym.borrow_mut().add_new_variable(session, oyarn!("{}", arg.name.id), &arg.range);
739-
param.borrow_mut().as_variable_mut().is_parameter = true;
740-
sym.borrow_mut().as_func_mut().args.push(Argument {
741-
symbol: Rc::downgrade(&param),
742-
default_value: None,
743-
arg_type: ArgumentType::VARARG,
744-
annotation: arg.annotation.clone(),
745-
});
746-
}
747-
for arg in func_def.parameters.kwonlyargs.iter() {
748-
let param = sym.borrow_mut().add_new_variable(session, oyarn!("{}", arg.parameter.name.id), &arg.range);
749-
param.borrow_mut().as_variable_mut().is_parameter = true;
750-
sym.borrow_mut().as_func_mut().args.push(Argument {
751-
symbol: Rc::downgrade(&param),
752-
default_value: arg.default.as_ref().map(|_default| Evaluation::new_none()),
753-
arg_type: ArgumentType::KWORD_ONLY,
754-
annotation: arg.parameter.annotation.clone(),
755-
});
756-
}
757-
if let Some(arg) = &func_def.parameters.kwarg {
758-
let param = sym.borrow_mut().add_new_variable(session, oyarn!("{}", arg.name.id), &arg.range);
759-
param.borrow_mut().as_variable_mut().is_parameter = true;
760-
sym.borrow_mut().as_func_mut().args.push(Argument {
761-
symbol: Rc::downgrade(&param),
762-
default_value: None,
763-
arg_type: ArgumentType::KWARG,
764-
annotation: arg.annotation.clone(),
765-
});
766-
}
774+
PythonArchBuilder::handle_func_args(&sym, session, &func_def.parameters);
767775
let mut add_noqa = false;
768776
if let Some(noqa_bloc) = self.file_info.as_ref().unwrap().borrow().noqas_blocs.get(&func_def.range.start().to_u32()) {
769777
session.noqas_stack.push(noqa_bloc.clone());

server/src/core/python_arch_eval.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,12 +314,26 @@ impl PythonArchEval {
314314
expr_slice.lower.as_ref().map(|lower_expr| self.visit_expr(session, &lower_expr));
315315
},
316316
// Expressions that cannot contained a named expressions are not traversed
317-
Expr::Lambda(_todo_lambda_expr) => {
317+
Expr::Lambda(lambda_expr) => {
318318
// Lambdas can have named expressions, but it is not a common use
319319
// Like lambda vals: vals[(x := 0): x + 3]
320320
// However x is only in scope in the lambda expression only
321321
// It needs adding a new function, ast_indexes, then add the variable inside
322322
// I deem it currently unnecessary
323+
let variable: Option<Rc<RefCell<Symbol>>> = self.sym_stack.last().unwrap().borrow().get_positioned_symbol(&Sy!("<lambda>"), &lambda_expr.range);
324+
let Some(lambda_fn_sym) = variable else {
325+
return; // can be not found if AST is incomplete
326+
};
327+
lambda_fn_sym.borrow_mut().as_func_mut().arch_eval_status = BuildStatus::IN_PROGRESS;
328+
self.sym_stack.push(lambda_fn_sym.clone());
329+
self.visit_expr(session, &lambda_expr.body);
330+
let mut deps = vec![vec![], vec![]];
331+
let (eval, diags) = Evaluation::eval_from_ast(session, &lambda_expr.body, lambda_fn_sym.clone(), &lambda_expr.body.range().start(), false, &mut deps);
332+
self.diagnostics.extend(diags);
333+
Symbol::insert_dependencies(&self.file, &mut deps, self.current_step);
334+
FunctionSymbol::add_return_evaluations(lambda_fn_sym.clone(), session, eval);
335+
self.sym_stack.pop();
336+
lambda_fn_sym.borrow_mut().as_func_mut().arch_eval_status = BuildStatus::DONE;
323337
},
324338
Expr::Generator(_todo_expr_generator) => {
325339
// generators are lazily evaluated,

server/src/core/python_arch_eval_hooks.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ static arch_eval_file_hooks: Lazy<Vec<PythonArchEvalFileHook>> = Lazy::new(|| {v
7373
let values: Vec<ruff_python_ast::Expr> = Vec::new();
7474
let mut id = symbol.borrow_mut();
7575
let range = id.range().clone();
76-
id.set_evaluations(vec![Evaluation::new_list(odoo.sync_odoo, values, range)]);
76+
id.set_evaluations(vec![Evaluation::new_list(odoo.sync_odoo, Some(values), range)]);
7777
}},
7878
/*PythonArchEvalFileHook {file_tree: vec![Sy!("odoo"), Sy!("models")],
7979
content_tree: vec![Sy!("BaseModel"), Sy!("search_count")],

server/tests/data/addons/module_1/models/base_test_models.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,16 @@ def _get_partner_id(self):
4848
class NoBaseModel(models.Model):
4949
_inherit = "module_1.no_base_model"
5050

51-
basic_var = 42
51+
basic_var = 42
52+
lambda_ref = lambda x: basic_var
53+
fstring_ref = f"value: {basic_var}"
54+
boolop_ref = basic_var and 1
55+
compare_ref = basic_var > 0
56+
listcomp_ref = [basic_var for _ in []]
57+
dictcomp_ref = {basic_var: 1 for _ in []}
58+
list_ref = [basic_var, 1]
59+
tuple_ref = (basic_var, 1)
60+
set_ref = {basic_var}
61+
dict_ref = {1: basic_var}
62+
call_ref = print(basic_var)
63+
binop_ref = basic_var + 1

server/tests/test_get_symbol.rs

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Test the hover feature by calling get_hover on various symbols in the test addons.
1+
// Test the hover feature by calling get_hover on various symbols in the test addons.
22

33
use odoo_ls_server::core::odoo::SyncOdoo;
44
use odoo_ls_server::utils::{PathSanitizer, ToFilePath};
@@ -278,4 +278,145 @@ fn test_model_subscription() {
278278
"Resolving a subscript of a model should include the model symbol itself.
279279
Expected to find BaseTestModel symbol among resolved symbols of `partner = self.search([], limit=2)[-1:]`"
280280
)
281-
}
281+
}
282+
283+
#[test]
284+
/// Test that symbol resolution (hover/get_symbol) works inside newly-handled AST constructs:
285+
/// lambda bodies, f-strings, bool operations, comparisons, list comprehensions, and dict comprehensions.
286+
fn test_get_symbol_in_new_expression_constructs() {
287+
let (mut odoo, config) = setup::setup::setup_server(true);
288+
let test_addons_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests").join("data").join("addons");
289+
let test_file = test_addons_path.join("module_1").join("models").join("base_test_models.py").sanitize();
290+
assert!(PathBuf::from(&test_file).exists(), "Test file does not exist: {}", test_file);
291+
let mut session = setup::setup::create_init_session(&mut odoo, config);
292+
293+
let file_mgr = session.sync_odoo.get_file_mgr();
294+
let file_info = file_mgr.borrow().get_file_info(&test_file).unwrap();
295+
let Some(file_symbol) = SyncOdoo::get_symbol_of_opened_file(
296+
&mut session,
297+
&PathBuf::from(&test_file)
298+
) else {
299+
panic!("Failed to get file symbol");
300+
};
301+
302+
// Hover on `basic_var` inside the lambda body: `lambda_ref = lambda x: basic_var`
303+
// The reference is at line 51, col 23
304+
let hover = test_utils::get_hover_markdown(&mut session, &file_symbol, &file_info, 51, 23)
305+
.unwrap_or_default();
306+
assert!(
307+
hover.contains("basic_var"),
308+
"Hover inside lambda body should resolve to basic_var, got: {hover}"
309+
);
310+
assert!(
311+
hover.contains("int"),
312+
"basic_var inside lambda body should have type int, got: {hover}"
313+
);
314+
315+
// Hover on the lambda parameter `x` in `lambda x: basic_var`
316+
// The parameter is at line 51, col 20
317+
let hover_param = test_utils::get_hover_markdown(&mut session, &file_symbol, &file_info, 51, 20)
318+
.unwrap_or_default();
319+
assert!(
320+
!hover_param.is_empty(),
321+
"Hover on lambda parameter should return a result"
322+
);
323+
324+
// Hover on `basic_var` inside an f-string: `fstring_ref = f"value: {basic_var}"`
325+
// The reference is at line 52, col 24
326+
let hover = test_utils::get_hover_markdown(&mut session, &file_symbol, &file_info, 52, 24)
327+
.unwrap_or_default();
328+
assert!(
329+
hover.contains("basic_var"),
330+
"Hover inside f-string should resolve to basic_var, got: {hover}"
331+
);
332+
333+
// Hover on `basic_var` inside a bool operation: `boolop_ref = basic_var and 1`
334+
// The reference is at line 53, col 13
335+
let hover = test_utils::get_hover_markdown(&mut session, &file_symbol, &file_info, 53, 13)
336+
.unwrap_or_default();
337+
assert!(
338+
hover.contains("basic_var"),
339+
"Hover inside bool operation should resolve to basic_var, got: {hover}"
340+
);
341+
342+
// Hover on `basic_var` inside a compare expression: `compare_ref = basic_var > 0`
343+
// The reference is at line 54, col 14
344+
let hover = test_utils::get_hover_markdown(&mut session, &file_symbol, &file_info, 54, 14)
345+
.unwrap_or_default();
346+
assert!(
347+
hover.contains("basic_var"),
348+
"Hover inside compare expression should resolve to basic_var, got: {hover}"
349+
);
350+
351+
// Hover on `basic_var` inside a list comprehension: `listcomp_ref = [basic_var for _ in []]`
352+
// The reference is at line 55, col 16
353+
let hover = test_utils::get_hover_markdown(&mut session, &file_symbol, &file_info, 55, 16)
354+
.unwrap_or_default();
355+
assert!(
356+
hover.contains("basic_var"),
357+
"Hover inside list comprehension should resolve to basic_var, got: {hover}"
358+
);
359+
360+
// Hover on `basic_var` in the key position of a dict comprehension: `dictcomp_ref = {basic_var: 1 for _ in []}`
361+
// The reference is at line 56, col 16
362+
let hover = test_utils::get_hover_markdown(&mut session, &file_symbol, &file_info, 56, 16)
363+
.unwrap_or_default();
364+
assert!(
365+
hover.contains("basic_var"),
366+
"Hover inside dict comprehension key should resolve to basic_var, got: {hover}"
367+
);
368+
369+
// Hover on `basic_var` inside a list literal: `list_ref = [basic_var, 1]`
370+
// The reference is at line 57, col 12
371+
let hover = test_utils::get_hover_markdown(&mut session, &file_symbol, &file_info, 57, 12)
372+
.unwrap_or_default();
373+
assert!(
374+
hover.contains("basic_var"),
375+
"Hover inside list literal should resolve to basic_var, got: {hover}"
376+
);
377+
378+
// Hover on `basic_var` inside a tuple literal: `tuple_ref = (basic_var, 1)`
379+
// The reference is at line 58, col 13
380+
let hover = test_utils::get_hover_markdown(&mut session, &file_symbol, &file_info, 58, 13)
381+
.unwrap_or_default();
382+
assert!(
383+
hover.contains("basic_var"),
384+
"Hover inside tuple literal should resolve to basic_var, got: {hover}"
385+
);
386+
387+
// Hover on `basic_var` inside a set literal: `set_ref = {basic_var}`
388+
// The reference is at line 59, col 11
389+
let hover = test_utils::get_hover_markdown(&mut session, &file_symbol, &file_info, 59, 11)
390+
.unwrap_or_default();
391+
assert!(
392+
hover.contains("basic_var"),
393+
"Hover inside set literal should resolve to basic_var, got: {hover}"
394+
);
395+
396+
// Hover on `basic_var` as a dict literal value: `dict_ref = {1: basic_var}`
397+
// The reference is at line 60, col 15
398+
let hover = test_utils::get_hover_markdown(&mut session, &file_symbol, &file_info, 60, 15)
399+
.unwrap_or_default();
400+
assert!(
401+
hover.contains("basic_var"),
402+
"Hover inside dict literal value should resolve to basic_var, got: {hover}"
403+
);
404+
405+
// Hover on `basic_var` as a call argument: `call_ref = print(basic_var)`
406+
// The reference is at line 61, col 17
407+
let hover = test_utils::get_hover_markdown(&mut session, &file_symbol, &file_info, 61, 17)
408+
.unwrap_or_default();
409+
assert!(
410+
hover.contains("basic_var"),
411+
"Hover on call argument should resolve to basic_var, got: {hover}"
412+
);
413+
414+
// Hover on `basic_var` as the left operand of a binary operator: `binop_ref = basic_var + 1`
415+
// The reference is at line 62, col 12
416+
let hover = test_utils::get_hover_markdown(&mut session, &file_symbol, &file_info, 62, 12)
417+
.unwrap_or_default();
418+
assert!(
419+
hover.contains("basic_var"),
420+
"Hover on binary operator operand should resolve to basic_var, got: {hover}"
421+
);
422+
}

0 commit comments

Comments
 (0)