use std::{borrow::Borrow, collections::HashMap, fmt::Debug, iter::FusedIterator, ops::ControlFlow, sync::Arc};
use lasso::Spur;
use ropey::Rope;
use tracing::{instrument, trace};
use tree_sitter::{Node, Parser, QueryCursor};
use crate::{
format_loc,
index::{Index, Symbol, _G, _R},
model::{Method, MethodReturnType, ModelEntry, ModelName, PropertyKind},
test_utils,
utils::{lsp_range_to_offset_range, ByteRange, Erase, PreTravel, RangeExt, TryResultExt},
ImStr,
};
use ts_macros::query;
pub static MODEL_METHODS: phf::Set<&[u8]> = phf::phf_set!(
b"create",
b"copy",
b"name_create",
b"browse",
b"filtered",
b"filtered_domain",
b"sorted",
b"search",
b"search_fetch",
b"name_search",
b"ensure_one",
b"with_context",
b"with_user",
b"with_company",
b"with_env",
b"sudo",
b"exists",
b"new",
b"save",
);
#[derive(Clone, Debug)]
pub enum Type {
Env,
RefFn,
ModelFn(ImStr),
Model(ImStr),
Record(ImStr),
Super,
Method(ModelName, ImStr),
Value,
}
#[derive(Default, Clone)]
pub struct Scope {
pub variables: HashMap<String, Type>,
pub parent: Option<Box<Scope>>,
pub super_: Option<ImStr>,
}
impl Debug for Scope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Scope").field(&"..").finish()
}
}
pub fn normalize<'r, 'n>(node: &'r mut Node<'n>) -> &'r mut Node<'n> {
let mut cursor = node.walk();
while matches!(
node.kind(),
"expression_statement" | "parenthesized_expression" | "module"
) {
let Some(child) = node.named_children(&mut cursor).find(|child| child.kind() != "comment") else {
break;
};
*node = child;
}
node
}
impl Scope {
fn new(parent: Option<Scope>) -> Self {
Self {
parent: parent.map(Box::new),
..Default::default()
}
}
pub fn get(&self, key: impl Borrow<str>) -> Option<&Type> {
fn impl_<'me>(self_: &'me Scope, key: &str) -> Option<&'me Type> {
self_
.variables
.get(key)
.or_else(|| self_.parent.as_ref().and_then(|parent| parent.get(key)))
}
impl_(self, key.borrow())
}
pub fn insert(&mut self, key: String, value: Type) {
self.variables.insert(key, value);
}
pub fn enter(&mut self, inherit_super: bool) {
*self = Scope::new(Some(core::mem::take(self)));
if inherit_super {
self.super_ = self.parent.as_ref().unwrap().super_.clone();
}
}
pub fn exit(&mut self) {
if let Some(parent) = self.parent.take() {
*self = *parent;
}
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &Type)> {
Iter {
variables: self.variables.iter(),
parent: self.parent.as_deref(),
}
}
}
struct Iter<'a> {
variables: std::collections::hash_map::Iter<'a, String, Type>,
parent: Option<&'a Scope>,
}
impl FusedIterator for Iter<'_> {}
impl<'a> Iterator for Iter<'a> {
type Item = (&'a str, &'a Type);
fn next(&mut self) -> Option<Self::Item> {
if let Some((key, value)) = self.variables.next() {
return Some((key.as_str(), value));
}
let parent = self.parent.take()?;
self.parent = parent.parent.as_deref();
self.variables = parent.variables.iter();
let (key, value) = self.variables.next()?;
Some((key.as_str(), value))
}
}
#[rustfmt::skip]
query! {
#[derive(Debug)]
FieldCompletion(Name, SelfParam, Scope);
((class_definition
(block
(expression_statement [
(assignment (identifier) @_name (string) @NAME)
(assignment (identifier) @_inherit (list . (string) @NAME)) ])?
[
(decorated_definition
(function_definition
(parameters . (identifier) @SELF_PARAM)) @SCOPE)
(function_definition (parameters . (identifier) @SELF_PARAM)) @SCOPE])) @class
(#eq? @_inherit "_inherit")
(#match? @_name "^_(name|inherit)$"))
}
#[rustfmt::skip]
query! {
MappedCall(Callee, Iter);
((call
(attribute (_) @CALLEE (identifier) @_mapped)
(argument_list [
(lambda (lambda_parameters . (identifier) @ITER))
(keyword_argument
(identifier) @_func
(lambda (lambda_parameters . (identifier) @ITER)))]))
(#match? @_func "^(func|key)$")
(#match? @_mapped "^(mapp|filter|sort)ed$"))
}
pub type ScopeControlFlow = ControlFlow<Option<Scope>, bool>;
impl Index {
#[inline]
pub fn model_of_range(&self, node: Node<'_>, range: ByteRange, contents: &[u8]) -> Option<ModelName> {
let (type_at_cursor, scope) = self.type_of_range(node, range, contents)?;
self.try_resolve_model(&type_at_cursor, &scope)
}
pub fn type_of_range(&self, node: Node<'_>, range: ByteRange, contents: &[u8]) -> Option<(Type, Scope)> {
trace!(target: "type_of_range", "{}", String::from_utf8_lossy(&contents[range.erase()]));
let (self_type, fn_scope, self_param) = determine_scope(node, contents, range.start.0)?;
let mut scope = Scope::default();
let self_type = match self_type {
Some(type_) => &contents[type_.byte_range().shrink(1)],
None => &[],
};
scope.super_ = Some(self_param.as_ref().into());
scope.insert(
self_param.into_owned(),
Type::Model(String::from_utf8_lossy(self_type).as_ref().into()),
);
let (_, Some(scope)) = Self::walk_scope(fn_scope, Some(scope), |scope, node| {
self.build_scope(scope, node, range.end.0, contents)
}) else {
return None;
};
let node_at_cursor = fn_scope.descendant_for_byte_range(range.start.0, range.end.0)?;
let type_at_cursor = self.type_of(node_at_cursor, &scope, contents)?;
Some((type_at_cursor, scope))
}
pub fn build_scope(&self, scope: &mut Scope, node: Node, offset: usize, contents: &[u8]) -> ScopeControlFlow {
if node.start_byte() > offset {
return ControlFlow::Break(Some(core::mem::take(scope)));
}
match node.kind() {
"assignment" | "named_expression" => {
let lhs = node.named_child(0).unwrap();
if lhs.kind() == "identifier" {
let rhs = lhs.next_named_sibling().unwrap();
if let Some(type_) = self.type_of(rhs, scope, contents) {
let lhs = String::from_utf8_lossy(&contents[lhs.byte_range()]);
scope.insert(lhs.into_owned(), type_);
}
}
}
"for_statement" => {
scope.enter(true);
let lhs = node.named_child(0).unwrap();
if lhs.kind() == "identifier" {
let rhs = lhs.next_named_sibling().unwrap();
if let Some(type_) = self.type_of(rhs, scope, contents) {
let lhs = String::from_utf8_lossy(&contents[lhs.byte_range()]);
scope.insert(lhs.into_owned(), type_);
}
}
return ControlFlow::Continue(true);
}
"function_definition" => {
let mut inherit_super = false;
let mut node = node;
while let Some(parent) = node.parent() {
match parent.kind() {
"decorated_definition" => {
node = parent;
continue;
}
"block" => inherit_super = parent.parent().is_some_and(|gp| gp.kind() == "class_definition"),
_ => {}
}
break;
}
scope.enter(inherit_super);
return ControlFlow::Continue(true);
}
"list_comprehension" | "set_comprehension" | "dictionary_comprehension" | "generator_expression"
if node.byte_range().contains(&offset) =>
{
let for_in = node.named_child(1).unwrap();
let lhs = for_in.named_child(0).unwrap();
if lhs.kind() == "identifier" {
let rhs = lhs.next_named_sibling().unwrap();
if let Some(type_) = self.type_of(rhs, scope, contents) {
let lhs = String::from_utf8_lossy(&contents[lhs.byte_range()]);
scope.insert(lhs.into_owned(), type_);
}
}
}
"call" if node.byte_range().contains_end(offset) => {
let query = MappedCall::query();
let mut cursor = QueryCursor::new();
if let Some(mapped_call) = cursor.matches(query, node, contents).next() {
let callee = mapped_call
.nodes_for_capture_index(MappedCall::Callee as _)
.next()
.unwrap();
if let Some(type_) = self.type_of(callee, scope, contents) {
let iter = mapped_call
.nodes_for_capture_index(MappedCall::Iter as _)
.next()
.unwrap();
let iter = String::from_utf8_lossy(&contents[iter.byte_range()]);
scope.insert(iter.into_owned(), type_);
}
}
}
"with_statement" => {
let as_pattern = node
.named_child(0)
.expect("with_clause")
.named_child(0)
.expect("with_item")
.named_child(0)
.expect("as_pattern");
if as_pattern.kind() == "as_pattern" {
let value = as_pattern.named_child(0).unwrap();
if let Some(target) = value.next_named_sibling() {
if target.kind() == "as_pattern_target" {
let alias = target.named_child(0).unwrap();
if alias.kind() == "identifier" && value.kind() == "call" {
let callee = value.named_child(0).expect("identifier");
if callee.kind() == "identifier" && b"Form" == &contents[callee.byte_range()] {
if let Some(first_arg) = value.named_child(1).unwrap().named_child(0) {
if let Some(type_) = self.type_of(first_arg, scope, contents) {
let alias =
String::from_utf8_lossy(&contents[alias.byte_range()]).into_owned();
scope.insert(alias, type_);
}
}
} else if let Some(type_) = self.type_of(value, scope, contents) {
let alias = String::from_utf8_lossy(&contents[alias.byte_range()]).into_owned();
scope.insert(alias, type_);
}
}
}
}
}
}
_ => {}
}
ControlFlow::Continue(false)
}
pub fn type_of(&self, mut node: Node, scope: &Scope, contents: &[u8]) -> Option<Type> {
#[cfg(debug_assertions)]
if node.byte_range().len() <= 64 {
trace!(
"type_of {} '{}'",
node.kind(),
String::from_utf8_lossy(&contents[node.byte_range()])
);
} else {
trace!("type_of {} range={:?}", node.kind(), node.byte_range());
}
match normalize(&mut node).kind() {
"subscript" => {
let lhs = node.child_by_field_name("value")?;
let rhs = node.child_by_field_name("subscript")?;
let obj_ty = self.type_of(lhs, scope, contents)?;
match obj_ty {
Type::Env if rhs.kind() == "string" => Some(Type::Model(
String::from_utf8_lossy(&contents[rhs.byte_range().shrink(1)])
.as_ref()
.into(),
)),
Type::Model(_) | Type::Record(_) => Some(obj_ty),
_ => None,
}
}
"attribute" => self.type_of_attribute_node(node, scope, contents),
"identifier" => {
let key = String::from_utf8_lossy(&contents[node.byte_range()]);
if key == "super" {
return Some(Type::Super);
}
scope.get(key).cloned()
}
"assignment" => {
let rhs = node.named_child(1)?;
self.type_of(rhs, scope, contents)
}
"call" => self.type_of_call_node(node, scope, contents),
"binary_operator" | "boolean_operator" => {
self.type_of(node.child_by_field_name("left")?, scope, contents)
}
_ => None,
}
}
fn type_of_call_node(&self, call: Node<'_>, scope: &Scope, contents: &[u8]) -> Option<Type> {
let func = call.named_child(0)?;
let func = self.type_of(func, scope, contents)?;
match func {
Type::RefFn => {
let xml_id = call.named_child(1)?.named_child(0)?;
matches!(xml_id.kind(), "string").then(|| {
Type::Record(
String::from_utf8_lossy(&contents[xml_id.byte_range().shrink(1)])
.as_ref()
.into(),
)
})
}
Type::ModelFn(model) => Some(Type::Model(model)),
Type::Super => scope.get(scope.super_.as_deref()?).cloned(),
Type::Method(model, mapped) if mapped == "mapped" => {
let mapped = call.named_child(1)?.named_child(0)?;
match mapped.kind() {
"string" => {
let mut model: Spur = model.into();
let mapped = String::from_utf8_lossy(&contents[mapped.byte_range().shrink(1)]);
let mut mapped = mapped.as_ref();
self.models.resolve_mapped(&mut model, &mut mapped, None).ok()?;
self.type_of_attribute(&Type::Model(_R(model).into()), mapped.as_bytes(), scope)
}
"lambda" => {
let mut scope = Scope::new(Some(scope.clone()));
if let Some(params) = mapped.child_by_field_name(b"parameters") {
let first_arg = params.named_child(0)?;
if first_arg.kind() == "identifier" {
let first_arg = String::from_utf8_lossy(&contents[first_arg.byte_range()]);
scope.insert(first_arg.into_owned(), Type::Model(_R(model).into()));
}
}
let body = mapped.child_by_field_name(b"body")?;
self.type_of(body, &scope, contents)
}
_ => None,
}
}
Type::Method(model, method) => {
let method = _G(&method)?;
let ret_model = self.resolve_method_returntype(method.into(), *model)?;
Some(Type::Model(_R(ret_model).into()))
}
Type::Env | Type::Record(..) | Type::Model(..) | Type::Value => None,
}
}
fn type_of_attribute_node(&self, attribute: Node<'_>, scope: &Scope, contents: &[u8]) -> Option<Type> {
let lhs = attribute.named_child(0)?;
let lhs = self.type_of(lhs, scope, contents)?;
let rhs = attribute.named_child(1)?;
match &contents[rhs.byte_range()] {
b"env" if matches!(lhs, Type::Model(..) | Type::Record(..)) => Some(Type::Env),
b"ref" if matches!(lhs, Type::Env) => Some(Type::RefFn),
b"user" if matches!(lhs, Type::Env) => Some(Type::Model("res.users".into())),
b"company" | b"companies" if matches!(lhs, Type::Env) => Some(Type::Model("res.company".into())),
b"mapped" => {
let model = self.try_resolve_model(&lhs, scope)?;
Some(Type::Method(model, "mapped".into()))
}
func if MODEL_METHODS.contains(func) => match lhs {
Type::Model(model) => Some(Type::ModelFn(model)),
Type::Record(xml_id) => {
let xml_id = _G(xml_id)?;
let record = self.records.get(&xml_id.into())?;
Some(Type::ModelFn(_R(*record.model.as_deref()?).into()))
}
_ => None,
},
ident if rhs.kind() == "identifier" => self.type_of_attribute(&lhs, ident, scope),
_ => None,
}
}
pub fn type_of_attribute(&self, type_: &Type, attr: &[u8], scope: &Scope) -> Option<Type> {
let model = self.try_resolve_model(type_, scope)?;
let attr = String::from_utf8_lossy(attr);
let model_entry = self.models.populate_properties(model, &[])?;
let attr_key = _G(attr.as_ref())?;
let attr_kind = model_entry.prop_kind(attr_key)?;
match attr_kind {
PropertyKind::Field => {
drop(model_entry);
let relation = self.models.resolve_related_field(attr_key.into(), model.into())?;
Some(Type::Model(_R(relation).into()))
}
PropertyKind::Method => Some(Type::Method(model, attr.as_ref().into())),
}
}
pub fn has_attribute(&self, type_: &Type, attr: &[u8], scope: &Scope) -> bool {
(|| -> Option<()> {
let model = self.try_resolve_model(type_, scope)?;
let attr = String::from_utf8_lossy(attr);
let entry = self.models.populate_properties(model, &[])?;
let attr = _G(attr.as_ref())?;
entry.prop_kind(attr).map(|_| ())
})()
.is_some()
}
pub fn try_resolve_model(&self, type_: &Type, scope: &Scope) -> Option<ModelName> {
match type_ {
Type::Model(model) => Some(_G(model)?.into()),
Type::Record(xml_id) => {
let xml_id = _G(xml_id)?;
let record = self.records.get(&xml_id.into())?;
record.model
}
Type::Super => self.try_resolve_model(scope.get(scope.super_.as_deref()?)?, scope),
_ => None,
}
}
pub fn walk_scope<T>(
node: Node,
scope: Option<Scope>,
mut step: impl FnMut(&mut Scope, Node) -> ControlFlow<Option<T>, bool>,
) -> (Scope, Option<T>) {
let mut scope = scope.unwrap_or_default();
let mut scope_ends = vec![];
for node in PreTravel::new(node) {
if !node.is_named() {
continue;
}
if let Some(end) = scope_ends.last() {
if node.start_byte() > *end {
scope.exit();
scope_ends.pop();
}
}
match step(&mut scope, node) {
ControlFlow::Break(value) => return (scope, value),
ControlFlow::Continue(entered) => {
if entered {
scope_ends.push(node.end_byte());
}
}
}
}
(scope, None)
}
pub fn resolve_method_returntype(&self, method: Symbol<Method>, model: Spur) -> Option<Symbol<ModelEntry>> {
#[cfg(debug_assertions)]
trace!(method = _R(method), model = _R(model), "resolve_method_returntype");
_ = self.models.populate_properties(model.into(), &[]);
let mut model_entry = self.models.get_mut(&model.into())?;
let method_obj = model_entry.methods.as_mut()?.get_mut(&method)?;
match method_obj.return_type {
MethodReturnType::Unprocessed => {}
MethodReturnType::Value | MethodReturnType::Processing => return None,
MethodReturnType::Relational(rel) => return Some(rel),
}
let location = method_obj.locations.first().cloned()?;
Arc::make_mut(method_obj).return_type = MethodReturnType::Processing;
drop(model_entry);
let contents = String::from_utf8(test_utils::fs::read(location.path.to_path()).unwrap()).unwrap();
let rope = Rope::from_str(&contents);
let contents = contents.as_bytes();
let mut parser = Parser::new();
parser.set_language(&tree_sitter_python::LANGUAGE.into()).unwrap();
let range = lsp_range_to_offset_range(location.range, &rope)?;
let ast = parser.parse(contents, None)?;
fn is_toplevel_return(mut node: Node) -> bool {
while node.kind() != "function_definition" {
tracing::info!("{}", node.kind());
match node.parent() {
Some(parent) => node = parent,
None => return false,
}
}
fn is_block_of_class(node: Node) -> bool {
node.kind() == "block" && node.parent().is_some_and(|parent| parent.kind() == "class_definition")
}
match node.parent() {
Some(decoration) if decoration.kind() == "decorated_definition" => {
return decoration.parent().is_some_and(is_block_of_class);
}
_ => {}
}
node.parent().is_some_and(is_block_of_class)
}
let (self_type, fn_scope, self_param) = determine_scope(ast.root_node(), contents, range.end.0)?;
let mut scope = Scope::default();
let self_type = match self_type {
Some(type_) => &contents[type_.byte_range().shrink(1)],
None => &[],
};
scope.super_ = Some(self_param.as_ref().into());
scope.insert(
self_param.into_owned(),
Type::Model(String::from_utf8_lossy(self_type).as_ref().into()),
);
let offset = fn_scope.end_byte();
let (_, type_) = Self::walk_scope(fn_scope, Some(scope), |scope, node| {
let entered = self.build_scope(scope, node, offset, contents).map_break(|_| None)?;
if node.kind() == "return_statement" && is_toplevel_return(node) {
let Some(child) = node.named_child(0) else {
return ControlFlow::Continue(entered);
};
let Some(type_) = self.type_of(child, scope, contents) else {
return ControlFlow::Continue(entered);
};
let Some(resolved) = self.try_resolve_model(&type_, scope) else {
return ControlFlow::Continue(entered);
};
return ControlFlow::Break(Some(resolved));
}
ControlFlow::Continue(entered)
});
let mut model = self.models.try_get_mut(&model.into()).expect(format_loc!("deadlock"))?;
let method = Arc::make_mut(model.methods.as_mut()?.get_mut(&method)?);
match type_ {
Some(rel) => method.return_type = MethodReturnType::Relational(rel),
None => method.return_type = MethodReturnType::Value,
}
let docstring = Self::method_docstring(fn_scope, contents)
.and_then(|doc| crate::str::Text::try_from(String::from_utf8_lossy(doc).as_ref()).ok());
method.docstring = docstring;
type_
}
fn method_docstring<'out>(fn_scope: Node, contents: &'out [u8]) -> Option<&'out [u8]> {
let block = fn_scope.child_by_field_name("body")?;
let expr_stmt = block.named_child(0)?;
if expr_stmt.kind() != "expression_statement" {
return None;
}
let string = expr_stmt.named_child(0)?;
if string.kind() != "string" {
return None;
}
let string_content = string.named_child(1)?;
if string_content.kind() != "string_content" {
return None;
}
Some(&contents[string_content.byte_range()])
}
}
#[instrument(level = "trace", skip_all, ret)]
pub fn determine_scope<'out, 'node>(
node: Node<'node>,
contents: &'out [u8],
offset: usize,
) -> Option<(Option<Node<'node>>, Node<'node>, std::borrow::Cow<'out, str>)> {
let query = FieldCompletion::query();
let mut self_type = None;
let mut self_param = None;
let mut fn_scope = None;
let mut cursor = QueryCursor::new();
'scoping: for match_ in cursor.matches(query, node, contents) {
let class = match_.captures.first()?;
if !class.node.byte_range().contains_end(offset) {
continue;
}
for capture in match_.captures {
match FieldCompletion::from(capture.index) {
Some(FieldCompletion::Name) => {
if self_type.is_none() {
self_type = Some(capture.node);
}
}
Some(FieldCompletion::SelfParam) => {
self_param = Some(capture.node);
}
Some(FieldCompletion::Scope) => {
if !capture.node.byte_range().contains_end(offset) {
continue 'scoping;
}
fn_scope = Some(capture.node);
}
None => {}
}
}
if fn_scope.is_some() {
break;
}
}
let fn_scope = fn_scope?;
let self_param = String::from_utf8_lossy(&contents[self_param?.byte_range()]);
Some((self_type, fn_scope, self_param))
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use ropey::Rope;
use tower_lsp_server::lsp_types::Position;
use tree_sitter::{Parser, QueryCursor};
use crate::analyze::FieldCompletion;
use crate::index::{_I, _R};
use crate::{index::Index, test_utils::cases::foo::prepare_foo_index, utils::position_to_offset};
#[test]
fn test_field_completion() {
let mut parser = Parser::new();
parser.set_language(&tree_sitter_python::LANGUAGE.into()).unwrap();
let contents = br#"
class Foo(models.AbstractModel):
_name = 'foo'
_description = 'What?'
_inherit = 'inherit_foo'
foo = fields.Char(related='related')
@api.depends('mapped')
def foo(self):
pass
"#;
let ast = parser.parse(&contents[..], None).unwrap();
let query = FieldCompletion::query();
let mut cursor = QueryCursor::new();
let actual = cursor
.matches(query, ast.root_node(), &contents[..])
.map(|match_| {
match_
.captures
.iter()
.map(|capture| FieldCompletion::from(capture.index))
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
let actual = actual.iter().map(Vec::as_slice).collect::<Vec<_>>();
use FieldCompletion as T;
assert!(
matches!(
&actual[..],
[
[None, None, Some(T::Name), Some(T::Scope), Some(T::SelfParam)],
[None, None, Some(T::Name), Some(T::Scope), Some(T::SelfParam)]
]
),
"{actual:?}"
)
}
#[test]
fn test_determine_scope() {
let mut parser = Parser::new();
parser.set_language(&tree_sitter_python::LANGUAGE.into()).unwrap();
let contents = r#"
class Foo(models.Model):
_name = 'foo'
def scope(self):
pass
"#;
let ast = parser.parse(contents, None).unwrap();
let rope = Rope::from(contents);
let fn_start = position_to_offset(Position { line: 3, character: 1 }, &rope).unwrap();
let fn_scope = ast
.root_node()
.named_descendant_for_byte_range(fn_start.0, fn_start.0)
.unwrap();
super::determine_scope(ast.root_node(), contents.as_bytes(), fn_start.0)
.unwrap_or_else(|| panic!("{}", fn_scope.to_sexp()));
}
#[test]
fn test_resolve_method_returntype() {
let index = Index {
models: prepare_foo_index(),
..Default::default()
};
assert_eq!(
index.resolve_method_returntype(_I("test").into(), _I("bar")).map(_R),
Some("foo")
)
}
#[test]
fn test_super_analysis() {
let index = Index {
models: prepare_foo_index(),
..Default::default()
};
assert_eq!(
index.resolve_method_returntype(_I("test").into(), _I("quux")).map(_R),
Some("foo")
)
}
}