[IMP] Implement Go To References Part 1 - python code#531
[IMP] Implement Go To References Part 1 - python code#531
Conversation
b6d04ff to
74786b2
Compare
a776bf4 to
b0ea6b5
Compare
|
|
8 similar comments
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
b8b89b9 to
5ede711
Compare
|
|
5ede711 to
b937cb6
Compare
|
|
mmahrouss
left a comment
There was a problem hiding this comment.
Looks good, I added some cosmetic suggestions, nothing functional:D
| 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); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The match is not needed here, we can drop it
| 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); | |
| } | |
| } | |
| } | |
| if odoo.evaluation_search.is_some() { | |
| 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); | |
| } |
| @@ -1468,14 +1604,28 @@ impl Odoo { | |||
| match ast_type { | |||
There was a problem hiding this comment.
Suggestion to have less code duplication and readability
| match ast_type { | |
| if file_info.borrow().file_info_ast.borrow().indexed_module.is_none() && matches!(ast_type, AstType::Python) { | |
| return Ok(None); | |
| } | |
| let location_fn = match (ast_type, is_declaration) { | |
| (AstType::Python, true) => DeclarationFeature::get_location, | |
| (AstType::Python, false) => DefinitionFeature::get_location, | |
| (AstType::Xml, true) => DeclarationFeature::get_location_xml, | |
| (AstType::Xml, false) => DefinitionFeature::get_location_xml, | |
| (AstType::Csv, true) => DeclarationFeature::get_location_csv, | |
| (AstType::Csv, false) => DefinitionFeature::get_location_csv, | |
| }; | |
| return Ok(location_fn(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); |
| Some(GotoDefinitionResponse::Link(links)) | ||
| } | ||
|
|
||
| pub fn get_location_xml(session: &mut SessionInfo, |
There was a problem hiding this comment.
could be useful to isolate get_location_xml from declaration and definition to goto_utils, to have less code duplication. what do you think?
| // for r in references.iter() { | ||
| // error!("Reference found at {}:{}:{}", r.uri.as_str(), r.range.start.line, r.range.start.character); | ||
| // } | ||
| } |
| let Some(symbol) = w.weak.upgrade() else { | ||
| res.push(current_sym.clone()); | ||
| continue; | ||
| }; |
There was a problem hiding this comment.
Why are we adding the sym to res in the else? it is an expired weak, so it is better to just continue, right?
There was a problem hiding this comment.
If we can't go further in the evaluations, we want to still keep the actual result, so the else is adding it to the list. Seems correct to me.
There was a problem hiding this comment.
I was about to comment the same thing. Keeping an EvaluationSymbolPtr::WEAK to an expired weak is useful? Why?
| 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; | ||
| } |
There was a problem hiding this comment.
We could move the check of found_one_reference and break early to avoid extra checks
| 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; | |
| } | |
| for eval in evals.iter() { | |
| 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 | |
| break; | |
| } | |
| if eval.symbol.sym.is_weak() && let Some(weak) = eval.symbol.sym.as_weak().weak.upgrade() { | |
| if Rc::ptr_eq(&weak, evaluation_search) { |
| /// 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) |
There was a problem hiding this comment.
This comments are no longer correct, no? perhaps only XML refs are missing?
| 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(); | ||
| } |
There was a problem hiding this comment.
We could isolate this into its own method visit_function_def to be consistent with the other branches, what do you think?
| let var_name = if alias.asname.is_none() { | ||
| S!(alias.name.split(".").next().unwrap()) | ||
| } else { | ||
| alias.asname.as_ref().unwrap().clone().to_string() | ||
| }; |
There was a problem hiding this comment.
Could be a bit more readable to use match here.
| let var_name = if alias.asname.is_none() { | |
| S!(alias.name.split(".").next().unwrap()) | |
| } else { | |
| alias.asname.as_ref().unwrap().clone().to_string() | |
| }; | |
| let var_name = match alias.asname.as_ref() { | |
| None => S!(alias.name.split(".").next().unwrap()), | |
| Some(asname) => S!(asname), | |
| }; |
cammarosano
left a comment
There was a problem hiding this comment.
Very nice!! Killer feature, nice approach!
I have added mostly minor style suggestions, feel free to ignore them.
|
|
||
| pub fn add_symbol(&mut self, content: &Rc<RefCell<Symbol>>, 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()); |
There was a problem hiding this comment.
or simply or_default()
| let sections = self.symbols.entry(content.borrow().name().clone()).or_insert(HashMap::default()); | |
| let sections = self.symbols.entry(content.borrow().name().clone()).or_default()); |
(same for function_symbol, file_symbol, etc)
|
|
||
| impl Hasher for U32IdentityHasher { | ||
| fn write(&mut self, bytes: &[u8]) { | ||
| // fallback lent (rare si on n'utilise que u32) |
| #[derive(Default)] | ||
| pub struct U32IdentityHasher(u64); | ||
|
|
||
| impl Hasher for U32IdentityHasher { |
There was a problem hiding this comment.
This looks cool!
But did you perf? Did it improve?
Have you considered using a Vector instead of Hashmap? If the idea is to use small sequential integers as keys, wouldn't a Vector be simpler (and cache friendlier)?
You could just type alias it to "SymbolMap" or something, ex:
type SymbolMap = Vec<u32, Vec<Rc<RefCell<Symbol>>>>or maybe wrap the inner Vec in an Option (to signal the absence/presence of entry for a given key), replicating what a map would return for get:
type SymbolMap = Vec<u32, Option<Vec<Rc<RefCell<Symbol>>>>>Your optimization makes the hashmap behave like a vector (no hashing, direct indexing), so why not go for a vector?
| 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(), |
There was a problem hiding this comment.
Very minor:
You replaced this expect with a unwrap (inside get_ts_list). I would suggest keeping the expect with an informative message.
(same for the other get_ts_*s below)
Also, see other comment about using a macro. That could be a way to bring the expect back.
| } | ||
| self.typeshed_weak_cache.object.clone() | ||
| } | ||
|
|
There was a problem hiding this comment.
Maybe a macro?
macro_rules! typeshed_getter {
($fn_name:ident, $field:ident, $($path:expr),+) => {
pub fn $fn_name(&mut self) -> Weak<RefCell<Symbol>> {
if self.typeshed_weak_cache.$field.is_expired() {
self.typeshed_weak_cache.$field = Rc::downgrade(
&self.get_symbol("", &(vec![Sy!("builtins")], vec![Sy!($($path),+)]), u32::MAX)
.last()
.expect(concat!("builtins ", stringify!($field), " not found"))
);
}
self.typeshed_weak_cache.$field.clone()
}
};
}
impl SyncOdoo {
typeshed_getter!(get_ts_dict, dict, "dict");
typeshed_getter!(get_ts_tuple, tuple, "tuple");
typeshed_getter!(get_ts_set, set, "set");
typeshed_getter!(get_ts_list, list, "list");
typeshed_getter!(get_ts_string, string, "str");
typeshed_getter!(get_ts_boolean, boolean, "bool");
typeshed_getter!(get_ts_int, int, "int");
typeshed_getter!(get_ts_float, float, "float");
typeshed_getter!(get_ts_complex, complex, "complex");
typeshed_getter!(get_ts_ellipsis, ellipsis, "Ellipsis");
typeshed_getter!(get_ts_bytes, bytes, "bytes");
typeshed_getter!(get_ts_object, object, "object");
}| 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 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 { | ||
| 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, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| _ => {} | ||
| } |
There was a problem hiding this comment.
| } | ||
| _ => {} | ||
| } | ||
| if let Some(evaluation_search) = session.sync_odoo.evaluation_search.as_ref() { |
There was a problem hiding this comment.
Beware of early returns in the code above this part!
A possible way to ensure this part always runs would be to make this a sibling function of current analyze_ast:
fn analyze_ast() -> result {
result = old_analyze_ast;
new_function_for_references(result.evals);
return result;
}And then found_one_reference would be declared inside thenew_function_for_references
Edit: maybe make new_function_for_references get called by eval_from_ast, right after the call to analyze_ast?
| 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::<Vec<String>>().join(", ") | ||
| ); | ||
|
|
There was a problem hiding this comment.
I suggest adding more tests 😬
Like functions/methods, shadowing/nested scopes... or when the lack of results is expected (like a literal).
| sym_stack: Vec<Rc<RefCell<Symbol>>>, | ||
| } | ||
|
|
||
| impl ReferenceVisitor { |
| 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{ |
There was a problem hiding this comment.
Maybe add a comment explaining what this deduplication of consecutive matches solves? (env["some_model"].sudo().browse() etc)
|
Hey! 👋 However, there is small areas that I'm not sure are broken due to the unimplemented list above in the description or something else. Here is an example of a function that partially works: def _log(self, uuid, account_id, data):
purpose = data['endpoint']
account_name = account_id.display_name if account_id else 'N/A'
self._create_account(uuid, account_id, data)
_logger.info('Request on behalf of %s for api_id %s to %s', uuid, account_name, purpose)
http.request.env['proxy.log'].sudo().create({
'uuid': uuid,
'account_id': account_id.id if account_id else False,
'purpose': purpose,
'is_paid': bool(self._check_paid(data)),
})Doing a get reference call on the variables will get the following info:
It seems like it can find use in top level calls but nothing within parameters of function calls that aren't on self. Goto def appears to work for all variables seen here even in areas get refs fail. Obviously this isn't merged yet so could still change but just wanted to put my experience in for the implementation. Thanks again, watching the growth of this has been great and excited for this feature once it is merged 😄 |
|
Hey @andg-odoo , Thank you for your feedback :) Indeed this PR is a 'part 1' to review and validate the architecture of the feature, but it is still missing some important things. And it's pointing us to some bugs too :D |

First part of the Go To References.
Include Python searches: python variables, models.
Do not search or work in: