Skip to content

[IMP] Implement Go To References Part 1 - python code#531

Open
fda-odoo wants to merge 9 commits intoalphafrom
alpha-references-fda
Open

[IMP] Implement Go To References Part 1 - python code#531
fda-odoo wants to merge 9 commits intoalphafrom
alpha-references-fda

Conversation

@fda-odoo
Copy link
Collaborator

@fda-odoo fda-odoo commented Jan 28, 2026

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

  • xml/csv files
  • manifest files
  • lambda functions

@fda-odoo fda-odoo self-assigned this Jan 28, 2026
@fda-odoo fda-odoo added the enhancement New feature or request label Jan 28, 2026
@fda-odoo fda-odoo added this to the 1.3.0 milestone Jan 28, 2026
@fda-odoo fda-odoo force-pushed the alpha-references-fda branch 2 times, most recently from b6d04ff to 74786b2 Compare February 3, 2026 22:29
@fda-odoo fda-odoo force-pushed the alpha-references-fda branch from a776bf4 to b0ea6b5 Compare February 10, 2026 10:20
@github-actions
Copy link

⚠️ The file 'Cargo.toml or constant.rs' has been modified in this PR. Please review the changes carefully.

8 similar comments
@github-actions
Copy link

⚠️ The file 'Cargo.toml or constant.rs' has been modified in this PR. Please review the changes carefully.

@github-actions
Copy link

⚠️ The file 'Cargo.toml or constant.rs' has been modified in this PR. Please review the changes carefully.

@github-actions
Copy link

⚠️ The file 'Cargo.toml or constant.rs' has been modified in this PR. Please review the changes carefully.

@github-actions
Copy link

⚠️ The file 'Cargo.toml or constant.rs' has been modified in this PR. Please review the changes carefully.

@github-actions
Copy link

⚠️ The file 'Cargo.toml or constant.rs' has been modified in this PR. Please review the changes carefully.

@github-actions
Copy link

⚠️ The file 'Cargo.toml or constant.rs' has been modified in this PR. Please review the changes carefully.

@github-actions
Copy link

⚠️ The file 'Cargo.toml or constant.rs' has been modified in this PR. Please review the changes carefully.

@github-actions
Copy link

⚠️ The file 'Cargo.toml or constant.rs' has been modified in this PR. Please review the changes carefully.

@fda-odoo fda-odoo force-pushed the alpha-references-fda branch from b8b89b9 to 5ede711 Compare February 24, 2026 10:13
@github-actions
Copy link

⚠️ The file 'Cargo.toml or constant.rs' has been modified in this PR. Please review the changes carefully.

@fda-odoo fda-odoo changed the title [IMP] Implement Go To References [IMP] Implement Go To References Part 1 - python code Feb 24, 2026
@fda-odoo fda-odoo force-pushed the alpha-references-fda branch from 5ede711 to b937cb6 Compare February 24, 2026 10:36
@github-actions
Copy link

⚠️ The file 'Cargo.toml or constant.rs' has been modified in this PR. Please review the changes carefully.

@fda-odoo fda-odoo requested a review from mmahrouss February 24, 2026 10:38
@fda-odoo fda-odoo marked this pull request as ready for review February 24, 2026 10:38
@fda-odoo fda-odoo requested a review from cammarosano February 24, 2026 10:38
@fda-odoo fda-odoo mentioned this pull request Feb 24, 2026
Copy link
Collaborator

@mmahrouss mmahrouss left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, I added some cosmetic suggestions, nothing functional:D

Comment on lines +1270 to 1277
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);
}
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The match is not needed here, we can drop it

Suggested change
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion to have less code duplication and readability

Suggested change
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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be useful to isolate get_location_xml from declaration and definition to goto_utils, to have less code duplication. what do you think?

Comment on lines +56 to +59
// for r in references.iter() {
// error!("Reference found at {}:{}:{}", r.uri.as_str(), r.range.start.line, r.range.start.character);
// }
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unneeded old comment?

Comment on lines +2364 to +2367
let Some(symbol) = w.weak.upgrade() else {
res.push(current_sym.clone());
continue;
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we adding the sym to res in the else? it is an expired weak, so it is better to just continue, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was about to comment the same thing. Keeping an EvaluationSymbolPtr::WEAK to an expired weak is useful? Why?

Comment on lines +1361 to +1367
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;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could move the check of found_one_reference and break early to avoid extra checks

Suggested change
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) {

Comment on lines +22 to +25
/// 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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comments are no longer correct, no? perhaps only XML refs are missing?

Comment on lines +165 to +170
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();
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could isolate this into its own method visit_function_def to be consistent with the other branches, what do you think?

Comment on lines +238 to +242
let var_name = if alias.asname.is_none() {
S!(alias.name.split(".").next().unwrap())
} else {
alias.asname.as_ref().unwrap().clone().to_string()
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be a bit more readable to use match here.

Suggested change
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),
};

Copy link
Collaborator

@cammarosano cammarosano left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or simply or_default()

Suggested change
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥖

#[derive(Default)]
pub struct U32IdentityHasher(u64);

impl Hasher for U32IdentityHasher {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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");
  }

Comment on lines +1387 to +1412
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,
});
}
}
}
}
},
_ => {}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every time
    you write code like this
        a puppy
            dies
                 !!!!
Image

😆

I suggest replacing this pattern

match value {
   the_one_pattern(x) => {...}
   _ => {}
}

By this:

let the_one_pattern(x) = value else {
     continue;
}
...

The same for if let without any code after it -> let ... else { continue }

}
_ => {}
}
if let Some(evaluation_search) = session.sync_odoo.evaluation_search.as_ref() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(", ")
);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is beautiful 💟

Comment on lines +1375 to +1378
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{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a comment explaining what this deduplication of consecutive matches solves? (env["some_model"].sudo().browse() etc)

@andg-odoo
Copy link

andg-odoo commented Mar 2, 2026

Hey! 👋
I did a quick test checking out this branch and just working as normal. It's great! 99% of the time it works perfectly with what I would use it for.

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:

  • uuid will find only the call in the parameter to self._create_account.
  • account_id will only find the calls on the assigning for account_name and self._create_account, but not the account_id inside of the create() call.
  • data will only find the calsl to assign purpose and self._create_account, but not the call inside of check_paid.

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 😄

@fda-odoo
Copy link
Collaborator Author

fda-odoo commented Mar 3, 2026

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
Aside of xml_id, manifests and lambda that are missing here, the branch is pointing out that some part of arguments are not processed by the tool. that's the case for some other expressions too. That's will be indeed part of the task once it's stabilized. I hope we'll be able to release it soon :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants