use ls_types::{Diagnostic, DiagnosticSeverity, Range};
use tree_sitter::Node;
use crate::config::ServerSettings;
use strsim::jaro_winkler;
use crate::grammar::{SPECIAL_VARIABLES, builtin_return_type, builtin_signature, renamed_builtin};
use crate::semantic::assign::{ObjectFault, Verdict, assignable, object_faults};
use crate::semantic::codes;
use crate::semantic::node_kind as k;
use crate::semantic::text::byte_range_to_lsp;
use crate::semantic::type_expr::TypeExpr;
use crate::semantic::types::{DocumentAnalysis, MergedSemanticModel};
const SOURCE: &str = "surreal-language-server";
pub struct TypeCtx<'a> {
pub model: &'a MergedSemanticModel,
pub source: &'a str,
pub bindings: &'a BindingTable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BindingKind {
Let,
FunctionParam,
ForLoop,
ClosureParam,
}
impl BindingKind {
pub fn label(self) -> &'static str {
match self {
Self::Let => "LET",
Self::FunctionParam => "parameter",
Self::ForLoop => "FOR",
Self::ClosureParam => "closure parameter",
}
}
}
#[derive(Debug, Clone)]
pub struct Binding {
pub name: String,
pub ty: TypeExpr,
pub declared: Option<TypeExpr>,
pub decl_span: std::ops::Range<usize>,
pub scope: std::ops::Range<usize>,
pub kind: BindingKind,
}
#[derive(Debug, Clone, Default)]
pub struct BindingTable {
entries: Vec<Binding>,
}
fn covers(scope: &std::ops::Range<usize>, at: usize) -> bool {
at >= scope.start && at <= scope.end
}
impl BindingTable {
pub fn resolve(&self, name: &str, at: usize) -> Option<&Binding> {
self.entries
.iter()
.rev()
.find(|binding| binding.name == name && covers(&binding.scope, at))
}
pub fn at(&self, name: &str, at: usize) -> Option<&Binding> {
self.resolve(name, at).or_else(|| {
self.entries
.iter()
.rev()
.find(|binding| binding.name == name && covers(&binding.decl_span, at))
})
}
pub fn iter(&self) -> impl Iterator<Item = &Binding> {
self.entries.iter()
}
pub fn visible_at(&self, at: usize) -> Vec<&Binding> {
let mut seen = Vec::new();
let mut out: Vec<&Binding> = Vec::new();
for binding in self.entries.iter().rev() {
if !covers(&binding.scope, at) || seen.contains(&binding.name) {
continue;
}
seen.push(binding.name.clone());
out.push(binding);
}
out
}
}
pub fn infer_expr_type(node: Node<'_>, ctx: &TypeCtx<'_>) -> TypeExpr {
let text = || k::text_of(ctx.source, node).unwrap_or_default();
match node.kind() {
k::STRING => TypeExpr::Scalar(string_kind(text()).to_string()),
k::FORMAT_STRING => TypeExpr::Scalar("string".to_string()),
k::REGEX => TypeExpr::Scalar("regex".to_string()),
k::DURATION => TypeExpr::Scalar("duration".to_string()),
k::BOOL => TypeExpr::Scalar("bool".to_string()),
k::NONE => TypeExpr::Scalar("none".to_string()),
k::POINT => TypeExpr::Scalar("point".to_string()),
k::INT => TypeExpr::Scalar("int".to_string()),
k::FLOAT => TypeExpr::Scalar("float".to_string()),
k::DECIMAL => TypeExpr::Scalar("decimal".to_string()),
k::NUMBER => k::named_children(node)
.first()
.map(|inner| infer_expr_type(*inner, ctx))
.unwrap_or(TypeExpr::Unknown),
k::ARRAY => TypeExpr::Array(Box::new(join_types(
k::named_children(node)
.into_iter()
.filter(|child| !is_trivia(*child))
.map(|child| infer_expr_type(child, ctx))
.collect(),
))),
k::SET => TypeExpr::Set(Box::new(join_types(
k::named_children(node)
.into_iter()
.filter(|child| !is_trivia(*child))
.map(|child| infer_expr_type(child, ctx))
.collect(),
))),
k::OBJECT => object_literal_type(node, ctx),
k::RECORD_ID | k::RANGE_RECORD_ID => k::find_child(node, k::RECORD_TB_IDENT)
.and_then(|table| k::text_of(ctx.source, table))
.map(|table| TypeExpr::Record(vec![table.to_string()]))
.unwrap_or_else(|| TypeExpr::Record(Vec::new())),
k::TYPE_CAST => k::named_children(node)
.into_iter()
.find(|child| k::TYPE_KINDS.contains(&child.kind()))
.map(|ty| TypeExpr::from_node(ty, ctx.source))
.unwrap_or(TypeExpr::Unknown),
k::FUNCTION_CALL => call_return_type(node, ctx),
k::SUB_QUERY => k::named_children(node)
.into_iter()
.find(|child| !is_trivia(*child))
.map(|inner| infer_expr_type(inner, ctx))
.unwrap_or(TypeExpr::Unknown),
k::PREFIX_EXPRESSION => TypeExpr::Scalar("bool".to_string()),
k::VARIABLE_NAME => ctx
.bindings
.resolve(text(), node.start_byte())
.map(|binding| binding.ty.clone())
.unwrap_or(TypeExpr::Unknown),
_ => TypeExpr::Unknown,
}
}
pub fn resolve_bindings(analysis: &DocumentAnalysis, model: &MergedSemanticModel) -> BindingTable {
let mut table = BindingTable::default();
let root = analysis.tree.root_node();
collect_bindings(root, root.end_byte(), &analysis.text, model, &mut table);
table
}
fn collect_bindings(
node: Node<'_>,
scope_end: usize,
source: &str,
model: &MergedSemanticModel,
table: &mut BindingTable,
) {
match node.kind() {
k::LET_STATEMENT => {
bind_let(node, scope_end, source, model, table);
return;
}
k::DEFINE_STATEMENT => {
if let Some(body) = k::find_child(node, k::BLOCK) {
bind_param_definitions(node, &body, BindingKind::FunctionParam, source, table);
}
}
k::FOR_STATEMENT => {
bind_for(node, source, model, table);
}
k::CLOSURE => {
bind_param_definitions(node, &node, BindingKind::ClosureParam, source, table);
}
_ => {}
}
let inner_end = if node.kind() == k::BLOCK {
node.end_byte()
} else {
scope_end
};
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
collect_bindings(child, inner_end, source, model, table);
}
}
fn bind_let(
node: Node<'_>,
scope_end: usize,
source: &str,
model: &MergedSemanticModel,
table: &mut BindingTable,
) {
let children = k::named_children(node);
let Some(definition) = children
.iter()
.find(|child| child.kind() == k::PARAM_DEFINITION)
else {
return;
};
let Some((name, declared, name_node)) = param_definition_parts(*definition, source) else {
return;
};
let value = children
.iter()
.find(|child| {
child.kind() != k::PARAM_DEFINITION && !k::is_keyword(**child) && !is_trivia(**child)
})
.copied();
let inferred = {
let ctx = TypeCtx {
model,
source,
bindings: table,
};
value
.map(|value| infer_expr_type(value, &ctx))
.unwrap_or(TypeExpr::Unknown)
};
if let Some(value) = value {
collect_bindings(value, scope_end, source, model, table);
}
table.entries.push(Binding {
name,
ty: declared.clone().unwrap_or(inferred),
declared,
decl_span: name_node.0..name_node.1,
scope: node.end_byte()..scope_end.max(node.end_byte()),
kind: BindingKind::Let,
});
}
fn bind_for(node: Node<'_>, source: &str, model: &MergedSemanticModel, table: &mut BindingTable) {
let children = k::named_children(node);
let Some(name_node) = children
.iter()
.find(|child| child.kind() == k::VARIABLE_NAME)
else {
return;
};
let Some(name) = k::text_of(source, *name_node) else {
return;
};
let body = k::find_child(node, k::BLOCK);
let iterable = children
.iter()
.find(|child| {
child.kind() != k::VARIABLE_NAME
&& child.kind() != k::BLOCK
&& !k::is_keyword(**child)
&& !is_trivia(**child)
})
.copied();
let element = {
let ctx = TypeCtx {
model,
source,
bindings: table,
};
match iterable.map(|node| infer_expr_type(node, &ctx)) {
Some(TypeExpr::Array(inner)) | Some(TypeExpr::Set(inner)) => *inner,
_ => TypeExpr::Unknown,
}
};
if let Some(body) = body {
table.entries.push(Binding {
name: name.to_string(),
ty: element,
declared: None,
decl_span: name_node.start_byte()..name_node.end_byte(),
scope: body.start_byte()..body.end_byte(),
kind: BindingKind::ForLoop,
});
}
}
fn bind_param_definitions(
owner: Node<'_>,
body: &Node<'_>,
kind: BindingKind,
source: &str,
table: &mut BindingTable,
) {
for definition in k::named_children(owner)
.into_iter()
.filter(|child| child.kind() == k::PARAM_DEFINITION)
{
let Some((name, declared, name_span)) = param_definition_parts(definition, source) else {
continue;
};
table.entries.push(Binding {
name,
ty: declared.clone().unwrap_or(TypeExpr::Unknown),
declared,
decl_span: name_span.0..name_span.1,
scope: body.start_byte()..body.end_byte(),
kind,
});
}
}
fn param_definition_parts(
definition: Node<'_>,
source: &str,
) -> Option<(String, Option<TypeExpr>, (usize, usize))> {
let children = k::named_children(definition);
let name_node = children
.iter()
.find(|child| child.kind() == k::VARIABLE_NAME)?;
let name = k::text_of(source, *name_node)?.to_string();
let declared = children
.iter()
.find(|child| k::TYPE_KINDS.contains(&child.kind()))
.map(|child| TypeExpr::from_node(*child, source));
Some((
name,
declared,
(name_node.start_byte(), name_node.end_byte()),
))
}
fn string_kind(text: &str) -> &'static str {
match text.as_bytes().first() {
Some(b'd') => "datetime",
Some(b'u') => "uuid",
Some(b'r') => "record",
Some(b'b') => "bytes",
Some(b'f') => "file",
_ => "string",
}
}
fn is_trivia(node: Node<'_>) -> bool {
matches!(node.kind(), k::COMMENT | k::BLOCK_COMMENT)
}
fn join_types(types: Vec<TypeExpr>) -> TypeExpr {
let mut iter = types.into_iter();
let Some(first) = iter.next() else {
return TypeExpr::Scalar("any".to_string());
};
if iter.all(|other| other == first) {
first
} else {
TypeExpr::Scalar("any".to_string())
}
}
fn object_literal_type(node: Node<'_>, ctx: &TypeCtx<'_>) -> TypeExpr {
let content = k::find_child(node, k::OBJECT_CONTENT).unwrap_or(node);
let fields: Vec<(String, TypeExpr)> = k::named_children(content)
.into_iter()
.filter(|child| child.kind() == k::OBJECT_PROPERTY)
.filter_map(|property| {
let children = k::named_children(property);
let key = children
.iter()
.find(|child| child.kind() == k::OBJECT_KEY)
.and_then(|child| {
let inner = k::named_children(*child);
let target = inner.first().copied().unwrap_or(*child);
k::text_of(ctx.source, target)
})
.map(|text| text.trim_matches(['"', '\'', '`']).to_string())?;
let value = children
.iter()
.find(|child| child.kind() != k::OBJECT_KEY && child.kind() != k::COLON)
.map(|child| infer_expr_type(*child, ctx))
.unwrap_or(TypeExpr::Unknown);
Some((key, value))
})
.collect();
TypeExpr::Object(fields)
}
fn call_return_type(node: Node<'_>, ctx: &TypeCtx<'_>) -> TypeExpr {
let Some(name) = callee_name(node, ctx.source) else {
return TypeExpr::Unknown;
};
if let Some(function) = ctx.model.functions.get(name) {
return function.return_type.clone().unwrap_or(TypeExpr::Unknown);
}
if let Some(refined) = refine_builtin_return(name, node, ctx) {
return refined;
}
builtin_return_type(name)
.cloned()
.unwrap_or(TypeExpr::Unknown)
}
fn refine_builtin_return(name: &str, call: Node<'_>, ctx: &TypeCtx<'_>) -> Option<TypeExpr> {
let normalized = name.trim().to_ascii_lowercase();
if !matches!(normalized.as_str(), "type::record" | "type::thing") {
return None;
}
let args = argument_nodes(k::find_child(call, k::ARGUMENT_LIST)?);
let literal = string_literal_text(*args.first()?, ctx.source)?;
let table = if args.len() == 1 {
literal.split_once(':')?.0
} else {
literal
};
let table = table.trim();
if table.is_empty() {
return None;
}
Some(TypeExpr::Record(vec![table.to_string()]))
}
fn string_literal_text<'a>(node: Node<'_>, source: &'a str) -> Option<&'a str> {
if node.kind() != k::STRING {
return None;
}
let text = k::text_of(source, node)?;
let mut chars = text.chars();
let quote = chars.next()?;
if !matches!(quote, '\'' | '"') {
return None;
}
text.strip_prefix(quote)?.strip_suffix(quote)
}
fn callee_name<'a>(node: Node<'_>, source: &'a str) -> Option<&'a str> {
let mut cursor = node.walk();
let name = node
.children(&mut cursor)
.find(|child| child.kind() == k::FUNCTION_NAME)?;
k::text_of(source, name)
}
fn contains_parse_error(node: Node<'_>) -> bool {
if node.is_error() || node.is_missing() {
return true;
}
node.has_error()
}
fn argument_nodes<'tree>(arg_list: Node<'tree>) -> Vec<Node<'tree>> {
let mut cursor = arg_list.walk();
arg_list
.named_children(&mut cursor)
.filter(|child| !is_trivia(*child))
.collect()
}
fn required_arity(params: &[crate::semantic::types::FunctionParam]) -> usize {
params
.iter()
.rposition(|param| !admits_none(param.type_expr.as_ref()))
.map(|index| index + 1)
.unwrap_or(0)
}
fn admits_none(declared: Option<&TypeExpr>) -> bool {
match declared {
Some(TypeExpr::Option(_)) => true,
Some(TypeExpr::Scalar(name)) => {
name.eq_ignore_ascii_case("any") || name.eq_ignore_ascii_case("value")
}
Some(TypeExpr::Union(members)) => members.iter().any(|member| match member {
TypeExpr::Scalar(name) => {
name.eq_ignore_ascii_case("none") || name.eq_ignore_ascii_case("null")
}
_ => false,
}),
_ => false,
}
}
pub fn type_diagnostics(
analysis: &DocumentAnalysis,
model: &MergedSemanticModel,
settings: &ServerSettings,
) -> Vec<Diagnostic> {
if !settings.analysis.enable_type_checking {
return Vec::new();
}
let bindings = resolve_bindings(analysis, model);
let ctx = TypeCtx {
model,
source: &analysis.text,
bindings: &bindings,
};
let mut diagnostics = Vec::new();
let root = analysis.tree.root_node();
check_calls(root, &ctx, &mut diagnostics);
check_let_annotations(root, &ctx, &mut diagnostics);
check_function_returns(root, &ctx, &mut diagnostics);
check_variables(root, &ctx, settings, &mut diagnostics);
diagnostics
}
fn check_function_returns(node: Node<'_>, ctx: &TypeCtx<'_>, out: &mut Vec<Diagnostic>) {
if node.kind() == k::DEFINE_STATEMENT
&& crate::semantic::analyzer::define_form(node, ctx.source).as_deref() == Some("function")
{
check_one_function_body(node, ctx, out);
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
check_function_returns(child, ctx, out);
}
}
fn check_one_function_body(node: Node<'_>, ctx: &TypeCtx<'_>, out: &mut Vec<Diagnostic>) {
let children = k::named_children(node);
let Some(declared) = crate::semantic::analyzer::function_return_type(&children, ctx.source)
else {
return;
};
let Some(body) = k::find_child(node, k::BLOCK) else {
return;
};
if contains_parse_error(body) {
return;
}
let name = k::find_child(node, k::FUNCTION_NAME)
.and_then(|node| k::text_of(ctx.source, node))
.unwrap_or("this function");
let mut returns = Vec::new();
collect_function_returns(body, &mut returns);
for statement in &returns {
if let Some(value) = returned_expression(*statement) {
report_return_mismatch(value, &declared, name, ctx, out);
}
}
let statements = k::named_children(body);
if let Some(tail) = statements.iter().rev().find(|child| {
!is_trivia(**child) && !matches!(child.kind(), k::BRACE_OPEN | k::BRACE_CLOSE)
}) && tail.kind() != k::RETURN_STATEMENT
{
report_return_mismatch(*tail, &declared, name, ctx, out);
}
}
fn collect_function_returns<'tree>(node: Node<'tree>, out: &mut Vec<Node<'tree>>) {
for child in k::named_children(node) {
if child.kind() == k::RETURN_STATEMENT {
out.push(child);
continue;
}
if propagates_return(child.kind()) {
collect_function_returns(child, out);
}
}
}
fn propagates_return(kind: &str) -> bool {
matches!(
kind,
k::BLOCK
| k::IF_ELSE_STATEMENT
| k::MODERN
| k::FOR_STATEMENT
)
}
fn returned_expression<'tree>(statement: Node<'tree>) -> Option<Node<'tree>> {
k::named_children(statement)
.into_iter()
.find(|child| !k::is_keyword(*child) && !is_trivia(*child))
}
fn report_return_mismatch(
value: Node<'_>,
declared: &TypeExpr,
name: &str,
ctx: &TypeCtx<'_>,
out: &mut Vec<Diagnostic>,
) {
let actual = infer_expr_type(value, ctx);
if assignable(&actual, declared) != Verdict::Incompatible {
return;
}
out.push(diagnostic(
node_range(ctx.source, value),
codes::RETURN_TYPE,
format!("`{name}` returns `{declared}`, but this value is `{actual}`."),
));
}
fn is_binding_site(node: Node<'_>) -> bool {
node.parent().is_some_and(|parent| {
matches!(
parent.kind(),
k::PARAM_DEFINITION | k::FOR_STATEMENT | k::DEFINE_STATEMENT
)
})
}
fn check_variables(
root: Node<'_>,
ctx: &TypeCtx<'_>,
settings: &ServerSettings,
out: &mut Vec<Diagnostic>,
) {
for node in variable_references(root) {
let Some(name) = k::text_of(ctx.source, node) else {
continue;
};
if is_bound(name, node.start_byte(), ctx, settings) {
continue;
}
let hint = match nearest_in_scope(name, node.start_byte(), ctx) {
Some(near) => format!(" Did you mean `{near}`?"),
None => String::new(),
};
out.push(diagnostic(
node_range(ctx.source, node),
codes::UNDEFINED_VARIABLE,
format!("`{name}` is not defined.{hint}"),
));
}
}
fn variable_references<'tree>(root: Node<'tree>) -> Vec<Node<'tree>> {
let mut found = Vec::new();
collect_variable_references(root, &mut found);
found
}
fn collect_variable_references<'tree>(node: Node<'tree>, out: &mut Vec<Node<'tree>>) {
if node.is_error() || node.is_missing() {
return;
}
if node.kind() == k::VARIABLE_NAME {
if !is_binding_site(node) {
out.push(node);
}
return;
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
collect_variable_references(child, out);
}
}
fn is_bound(name: &str, at: usize, ctx: &TypeCtx<'_>, settings: &ServerSettings) -> bool {
if ctx.bindings.at(name, at).is_some() || ctx.model.params.contains_key(name) {
return true;
}
if SPECIAL_VARIABLES
.iter()
.any(|(special, _)| special.eq_ignore_ascii_case(name))
{
return true;
}
let bare = name.trim_start_matches('$');
settings
.analysis
.external_params
.iter()
.any(|declared| declared.trim_start_matches('$').eq_ignore_ascii_case(bare))
}
fn nearest_in_scope(name: &str, at: usize, ctx: &TypeCtx<'_>) -> Option<String> {
let candidates = ctx
.bindings
.visible_at(at)
.into_iter()
.map(|binding| binding.name.clone())
.chain(ctx.model.params.keys().cloned())
.chain(
SPECIAL_VARIABLES
.iter()
.map(|(special, _)| (*special).to_string()),
);
candidates
.map(|candidate| {
let score = jaro_winkler(name, &candidate);
(candidate, score)
})
.filter(|(_, score)| *score > 0.86)
.max_by(|left, right| {
left.1
.partial_cmp(&right.1)
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(candidate, _)| candidate)
}
fn check_let_annotations(node: Node<'_>, ctx: &TypeCtx<'_>, out: &mut Vec<Diagnostic>) {
if node.kind() == k::LET_STATEMENT {
let children = k::named_children(node);
if let Some(definition) = children
.iter()
.find(|child| child.kind() == k::PARAM_DEFINITION)
&& let Some((name, Some(declared), _)) = param_definition_parts(*definition, ctx.source)
&& let Some(value) = children.iter().find(|child| {
child.kind() != k::PARAM_DEFINITION
&& !k::is_keyword(**child)
&& !is_trivia(**child)
})
{
let actual = infer_expr_type(*value, ctx);
if assignable(&actual, &declared).is_incompatible() {
out.push(diagnostic(
node_range(ctx.source, *value),
codes::LET_TYPE,
format!("`{name}` is declared `{declared}` but the value is `{actual}`."),
));
}
}
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
check_let_annotations(child, ctx, out);
}
}
fn check_calls(node: Node<'_>, ctx: &TypeCtx<'_>, out: &mut Vec<Diagnostic>) {
if node.kind() == k::FUNCTION_CALL {
check_one_call(node, ctx, out);
}
if node.kind() == k::IDIOM_FUNCTION {
check_method_call(node, ctx, out);
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
check_calls(child, ctx, out);
}
}
fn check_one_call(node: Node<'_>, ctx: &TypeCtx<'_>, out: &mut Vec<Diagnostic>) {
let Some(name) = callee_name(node, ctx.source) else {
return;
};
if node
.parent()
.is_some_and(|parent| parent.kind() == k::MIDDLEWARE_CLAUSE)
{
return;
}
if let Some(current) = renamed_builtin(name)
&& let Some(name_node) = k::find_child(node, k::FUNCTION_NAME)
{
out.push(warning(
node_range(ctx.source, name_node),
codes::RENAMED_FUNCTION,
format!("`{name}` has been renamed to `{current}`."),
));
}
let Some(arg_list) = k::find_child(node, k::ARGUMENT_LIST) else {
return;
};
if contains_parse_error(arg_list) {
return;
}
let args = argument_nodes(arg_list);
match ctx.model.functions.get(name) {
Some(function) => check_user_call(name, function, node, arg_list, &args, ctx, out),
None => check_builtin_call(name, arg_list, &args, ctx, out),
}
}
fn check_method_call(node: Node<'_>, ctx: &TypeCtx<'_>, out: &mut Vec<Diagnostic>) {
let Some(method) =
k::find_child(node, k::FUNCTION_NAME).and_then(|name| k::text_of(ctx.source, name))
else {
return;
};
let Some(arg_list) = k::find_child(node, k::ARGUMENT_LIST) else {
return;
};
if contains_parse_error(arg_list) {
return;
}
let Some(receiver) = method_receiver(node) else {
return;
};
let Some(namespace) = receiver_namespace(&infer_expr_type(receiver, ctx)) else {
return;
};
let name = format!("{namespace}::{method}");
let Some(signature) = builtin_signature(&name) else {
return;
};
if !signature.generated.signature_known || signature.generated.not_callable {
return;
}
if signature.generated.params.is_empty() {
return;
}
let args = argument_nodes(arg_list);
if args.is_empty() {
return;
}
let required = signature.required_arity().saturating_sub(1);
let maximum = signature.maximum_arity().map(|max| max.saturating_sub(1));
if args.len() < required || maximum.is_some_and(|max| args.len() > max) {
out.push(diagnostic(
node_range(ctx.source, arg_list),
codes::ARGUMENT_COUNT,
format!(
"`.{method}()` expects {}, found {}.",
expected_arity_label(required, maximum),
args.len()
),
));
return;
}
for (index, argument) in args.iter().enumerate() {
let Some(expected) = signature.param_type_at(index + 1) else {
continue;
};
let actual = infer_expr_type(*argument, ctx);
if assignable(&actual, expected) != Verdict::Incompatible {
continue;
}
out.push(diagnostic(
node_range(ctx.source, *argument),
codes::ARGUMENT_TYPE,
format!(
"Argument {} of `.{method}()` expects `{expected}`, found `{actual}`.",
index + 2
),
));
}
}
fn method_receiver<'tree>(idiom: Node<'tree>) -> Option<Node<'tree>> {
let subscript = idiom.parent()?;
let path = subscript.parent()?;
let children = k::named_children(path);
if children.len() < 2 || children[1].id() != subscript.id() {
return None;
}
children.first().copied()
}
fn receiver_namespace(ty: &TypeExpr) -> Option<&'static str> {
match ty {
TypeExpr::Array(_) => Some("array"),
TypeExpr::Set(_) => Some("set"),
TypeExpr::Object(_) => Some("object"),
TypeExpr::Scalar(name) => match name.to_ascii_lowercase().as_str() {
"string" => Some("string"),
"array" => Some("array"),
"set" => Some("set"),
"object" => Some("object"),
"duration" => Some("duration"),
"datetime" => Some("time"),
_ => None,
},
_ => None,
}
}
fn check_builtin_call(
name: &str,
arg_list: Node<'_>,
args: &[Node<'_>],
ctx: &TypeCtx<'_>,
out: &mut Vec<Diagnostic>,
) {
let Some(signature) = builtin_signature(name) else {
return;
};
if signature.generated.not_callable {
out.push(warning(
node_range(ctx.source, arg_list),
codes::NOT_CALLABLE,
format!(
"`{name}` parses, but SurrealDB has no implementation to call. \
The query will fail at run time."
),
));
return;
}
if !signature.generated.signature_known {
return;
}
if args.is_empty() {
return;
}
let required = signature.required_arity();
let maximum = signature.maximum_arity();
let too_few = args.len() < required;
let too_many = maximum.is_some_and(|max| args.len() > max);
if too_few || too_many {
out.push(diagnostic(
node_range(ctx.source, arg_list),
codes::ARGUMENT_COUNT,
format!(
"`{name}` expects {}, found {}.",
expected_arity_label(required, maximum),
args.len()
),
));
return;
}
for (index, argument) in args.iter().enumerate() {
let Some(expected) = signature.param_type_at(index) else {
continue;
};
let actual = infer_expr_type(*argument, ctx);
if assignable(&actual, expected) != Verdict::Incompatible {
continue;
}
out.push(diagnostic(
node_range(ctx.source, *argument),
codes::ARGUMENT_TYPE,
format!(
"Argument {} of `{name}` expects `{expected}`, found `{actual}`.",
index + 1
),
));
}
}
fn expected_arity_label(required: usize, maximum: Option<usize>) -> String {
match maximum {
None if required == 0 => "zero or more arguments".to_string(),
None => format!("{required} or more arguments"),
Some(0) => "no arguments".to_string(),
Some(max) if max == required && max == 1 => "1 argument".to_string(),
Some(max) if max == required => format!("{max} arguments"),
Some(max) => format!("{required} to {max} arguments"),
}
}
fn check_user_call(
name: &str,
function: &crate::semantic::types::FunctionDef,
_node: Node<'_>,
arg_list: Node<'_>,
args: &[Node<'_>],
ctx: &TypeCtx<'_>,
out: &mut Vec<Diagnostic>,
) {
let required = required_arity(&function.params);
if args.len() < required || args.len() > function.params.len() {
out.push(diagnostic(
node_range(ctx.source, arg_list),
codes::ARGUMENT_COUNT,
format!(
"`{name}` expects {} argument{}, found {}.",
arity_label(required, function.params.len()),
if function.params.len() == 1 { "" } else { "s" },
args.len()
),
));
return;
}
for (index, argument) in args.iter().enumerate() {
let Some(param) = function.params.get(index) else {
break;
};
let Some(expected) = param.type_expr.as_ref() else {
continue; };
let actual = infer_expr_type(*argument, ctx);
if assignable(&actual, expected) != Verdict::Incompatible {
continue;
}
if let (TypeExpr::Object(actual_fields), TypeExpr::Object(expected_fields)) =
(&actual, expected)
&& argument.kind() == k::OBJECT
{
report_object_faults(
*argument,
index + 1,
name,
actual_fields,
expected_fields,
ctx,
out,
);
continue;
}
out.push(diagnostic(
node_range(ctx.source, *argument),
codes::ARGUMENT_TYPE,
format!(
"Argument {} of `{name}` expects `{expected}`, found `{actual}`.",
index + 1
),
));
}
}
fn report_object_faults(
literal: Node<'_>,
position: usize,
name: &str,
actual_fields: &[(String, TypeExpr)],
expected_fields: &[(String, TypeExpr)],
ctx: &TypeCtx<'_>,
out: &mut Vec<Diagnostic>,
) {
for fault in object_faults(actual_fields, expected_fields) {
let (range, message) = match fault {
ObjectFault::Property {
key,
expected,
actual,
} => {
let range = property_value_node(literal, &key, ctx.source)
.map(|value| node_range(ctx.source, value))
.unwrap_or_else(|| node_range(ctx.source, literal));
(
range,
format!(
"Argument {position} of `{name}`: property `{key}` expects \
`{expected}`, found `{actual}`."
),
)
}
ObjectFault::Missing { key } => (
node_range(ctx.source, literal),
format!("Argument {position} of `{name}`: missing required property `{key}`."),
),
};
out.push(diagnostic(range, codes::ARGUMENT_TYPE, message));
}
}
fn property_value_node<'tree>(
literal: Node<'tree>,
key: &str,
source: &str,
) -> Option<Node<'tree>> {
let content = k::find_child(literal, k::OBJECT_CONTENT).unwrap_or(literal);
k::named_children(content)
.into_iter()
.filter(|child| child.kind() == k::OBJECT_PROPERTY)
.find_map(|property| {
let children = k::named_children(property);
let name = children
.iter()
.find(|child| child.kind() == k::OBJECT_KEY)
.and_then(|child| {
let inner = k::named_children(*child);
let target = inner.first().copied().unwrap_or(*child);
k::text_of(source, target)
})
.map(|text| text.trim_matches(['"', '\'', '`']))?;
if name != key {
return None;
}
children
.iter()
.find(|child| child.kind() != k::OBJECT_KEY && child.kind() != k::COLON)
.copied()
})
}
fn arity_label(required: usize, total: usize) -> String {
if required == total {
total.to_string()
} else {
format!("{required} to {total}")
}
}
fn node_range(source: &str, node: Node<'_>) -> Range {
byte_range_to_lsp(source, node.start_byte(), node.end_byte())
}
fn diagnostic(range: Range, code: &str, message: String) -> Diagnostic {
Diagnostic {
range,
severity: Some(DiagnosticSeverity::ERROR),
code: codes::as_code(code),
source: Some(SOURCE.to_string()),
message,
..Diagnostic::default()
}
}
fn warning(range: Range, code: &str, message: String) -> Diagnostic {
Diagnostic {
severity: Some(DiagnosticSeverity::WARNING),
..diagnostic(range, code, message)
}
}