slint/internal/compiler/passes/resolving.rs
Yuri Astrakhan feb7a864df Auto-fixed clippy::useless_format
See https://rust-lang.github.io/rust-clippy/master/index.html#useless_format

```
__CARGO_FIX_YOLO=1 cargo clippy --fix --all-targets --workspace --exclude gstreamer-player --exclude i-slint-backend-linuxkms --exclude uefi-demo --exclude ffmpeg -- -A clippy::all -W clippy::useless_format

cargo fmt --all
```

`__CARGO_FIX_YOLO=1` is a hack, but it does help a lot with the tedious fixes where the result is fairly clear.
2025-02-07 13:16:50 +01:00

1790 lines
74 KiB
Rust

// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
//! Passes that resolve the property binding expression.
//!
//! Before this pass, all the expression are of type Expression::Uncompiled,
//! and there should no longer be Uncompiled expression after this pass.
//!
//! Most of the code for the resolving actually lies in the expression_tree module
use crate::diagnostics::{BuildDiagnostics, Spanned};
use crate::expression_tree::*;
use crate::langtype::{ElementType, Struct, Type};
use crate::lookup::{LookupCtx, LookupObject, LookupResult, LookupResultCallable};
use crate::object_tree::*;
use crate::parser::{identifier_text, syntax_nodes, NodeOrToken, SyntaxKind, SyntaxNode};
use crate::typeregister::TypeRegister;
use core::num::IntErrorKind;
use smol_str::{SmolStr, ToSmolStr};
use std::collections::HashMap;
use std::rc::Rc;
/// This represents a scope for the Component, where Component is the repeated component, but
/// does not represent a component in the .slint file
#[derive(Clone)]
struct ComponentScope(Vec<ElementRc>);
fn resolve_expression(
expr: &mut Expression,
property_name: Option<&str>,
property_type: Type,
scope: &[ElementRc],
type_register: &TypeRegister,
type_loader: &crate::typeloader::TypeLoader,
diag: &mut BuildDiagnostics,
) {
if let Expression::Uncompiled(node) = expr {
let mut lookup_ctx = LookupCtx {
property_name,
property_type,
component_scope: scope,
diag,
arguments: vec![],
type_register,
type_loader: Some(type_loader),
current_token: None,
};
let new_expr = match node.kind() {
SyntaxKind::CallbackConnection => {
Expression::from_callback_connection(node.clone().into(), &mut lookup_ctx)
}
SyntaxKind::Function => Expression::from_function(node.clone().into(), &mut lookup_ctx),
SyntaxKind::Expression => {
//FIXME again: this happen for non-binding expression (i.e: model)
Expression::from_expression_node(node.clone().into(), &mut lookup_ctx)
.maybe_convert_to(lookup_ctx.property_type.clone(), node, diag)
}
SyntaxKind::BindingExpression => {
Expression::from_binding_expression_node(node.clone(), &mut lookup_ctx)
}
SyntaxKind::PropertyChangedCallback => Expression::from_codeblock_node(
syntax_nodes::PropertyChangedCallback::from(node.clone()).CodeBlock(),
&mut lookup_ctx,
),
SyntaxKind::TwoWayBinding => {
assert!(diag.has_errors(), "Two way binding should have been resolved already (property: {property_name:?})");
Expression::Invalid
}
_ => {
debug_assert!(diag.has_errors());
Expression::Invalid
}
};
*expr = new_expr;
}
}
/// Call the visitor for each children of the element recursively, starting with the element itself
///
/// The item that is being visited will be pushed to the scope and popped once visitation is over.
fn recurse_elem_with_scope(
elem: &ElementRc,
mut scope: ComponentScope,
vis: &mut impl FnMut(&ElementRc, &ComponentScope),
) -> ComponentScope {
scope.0.push(elem.clone());
vis(elem, &scope);
for sub in &elem.borrow().children {
scope = recurse_elem_with_scope(sub, scope, vis);
}
scope.0.pop();
scope
}
pub fn resolve_expressions(
doc: &Document,
type_loader: &crate::typeloader::TypeLoader,
diag: &mut BuildDiagnostics,
) {
resolve_two_way_bindings(doc, &doc.local_registry, diag);
for component in doc.inner_components.iter() {
recurse_elem_with_scope(
&component.root_element,
ComponentScope(vec![]),
&mut |elem, scope| {
let mut is_repeated = elem.borrow().repeated.is_some();
visit_element_expressions(elem, |expr, property_name, property_type| {
let scope = if is_repeated {
// The first expression is always the model and it needs to be resolved with the parent scope
debug_assert!(matches!(
elem.borrow().repeated.as_ref().unwrap().model,
Expression::Invalid
)); // should be Invalid because it is taken by the visit_element_expressions function
is_repeated = false;
debug_assert!(scope.0.len() > 1);
&scope.0[..scope.0.len() - 1]
} else {
&scope.0
};
resolve_expression(
expr,
property_name,
property_type(),
scope,
&doc.local_registry,
type_loader,
diag,
);
});
},
);
}
}
/// To be used in [`Expression::from_qualified_name_node`] to specify if the lookup is performed
/// for two ways binding (which happens before the models and other expressions are resolved),
/// or after that.
#[derive(Default)]
enum LookupPhase {
#[default]
UnspecifiedPhase,
ResolvingTwoWayBindings,
}
impl Expression {
pub fn from_binding_expression_node(node: SyntaxNode, ctx: &mut LookupCtx) -> Self {
debug_assert_eq!(node.kind(), SyntaxKind::BindingExpression);
let e = node
.children()
.find_map(|n| match n.kind() {
SyntaxKind::Expression => Some(Self::from_expression_node(n.into(), ctx)),
SyntaxKind::CodeBlock => Some(Self::from_codeblock_node(n.into(), ctx)),
_ => None,
})
.unwrap_or(Self::Invalid);
if ctx.property_type == Type::LogicalLength && e.ty() == Type::Percent {
// See if a conversion from percentage to length is allowed
const RELATIVE_TO_PARENT_PROPERTIES: &[&str] =
&["width", "height", "preferred-width", "preferred-height"];
let property_name = ctx.property_name.unwrap_or_default();
if RELATIVE_TO_PARENT_PROPERTIES.contains(&property_name) {
return e;
} else {
ctx.diag.push_error(
format!(
"Automatic conversion from percentage to length is only possible for the following properties: {}",
RELATIVE_TO_PARENT_PROPERTIES.join(", ")
),
&node
);
return Expression::Invalid;
}
};
if !matches!(ctx.property_type, Type::Callback { .. } | Type::Function { .. }) {
e.maybe_convert_to(ctx.property_type.clone(), &node, ctx.diag)
} else {
// Binding to a callback or function shouldn't happen
assert!(ctx.diag.has_errors());
e
}
}
fn from_codeblock_node(node: syntax_nodes::CodeBlock, ctx: &mut LookupCtx) -> Expression {
debug_assert_eq!(node.kind(), SyntaxKind::CodeBlock);
let mut statements_or_exprs = node
.children()
.filter_map(|n| match n.kind() {
SyntaxKind::Expression => Some(Self::from_expression_node(n.into(), ctx)),
SyntaxKind::ReturnStatement => Some(Self::from_return_statement(n.into(), ctx)),
_ => None,
})
.collect::<Vec<_>>();
let exit_points_and_return_types = statements_or_exprs
.iter()
.enumerate()
.filter_map(|(index, statement_or_expr)| {
if index == statements_or_exprs.len()
|| matches!(statement_or_expr, Expression::ReturnStatement(..))
{
Some((index, statement_or_expr.ty()))
} else {
None
}
})
.collect::<Vec<_>>();
let common_return_type = Self::common_target_type_for_type_list(
exit_points_and_return_types.iter().map(|(_, ty)| ty.clone()),
);
exit_points_and_return_types.into_iter().for_each(|(index, _)| {
let mut expr = std::mem::replace(&mut statements_or_exprs[index], Expression::Invalid);
expr = expr.maybe_convert_to(common_return_type.clone(), &node, ctx.diag);
statements_or_exprs[index] = expr;
});
Expression::CodeBlock(statements_or_exprs)
}
fn from_return_statement(
node: syntax_nodes::ReturnStatement,
ctx: &mut LookupCtx,
) -> Expression {
let return_type = ctx.return_type().clone();
let e = node.Expression();
if e.is_none() && !matches!(return_type, Type::Void | Type::Invalid) {
ctx.diag.push_error(format!("Must return a value of type '{return_type}'"), &node);
}
Expression::ReturnStatement(e.map(|n| {
Box::new(Self::from_expression_node(n, ctx).maybe_convert_to(
return_type,
&node,
ctx.diag,
))
}))
}
fn from_callback_connection(
node: syntax_nodes::CallbackConnection,
ctx: &mut LookupCtx,
) -> Expression {
ctx.arguments =
node.DeclaredIdentifier().map(|x| identifier_text(&x).unwrap_or_default()).collect();
Self::from_codeblock_node(node.CodeBlock(), ctx).maybe_convert_to(
ctx.return_type().clone(),
&node,
ctx.diag,
)
}
fn from_function(node: syntax_nodes::Function, ctx: &mut LookupCtx) -> Expression {
ctx.arguments = node
.ArgumentDeclaration()
.map(|x| identifier_text(&x.DeclaredIdentifier()).unwrap_or_default())
.collect();
Self::from_codeblock_node(node.CodeBlock(), ctx).maybe_convert_to(
ctx.return_type().clone(),
&node,
ctx.diag,
)
}
fn from_expression_node(node: syntax_nodes::Expression, ctx: &mut LookupCtx) -> Self {
node.children_with_tokens()
.find_map(|child| match child {
NodeOrToken::Node(node) => match node.kind() {
SyntaxKind::Expression => Some(Self::from_expression_node(node.into(), ctx)),
SyntaxKind::AtImageUrl => Some(Self::from_at_image_url_node(node.into(), ctx)),
SyntaxKind::AtGradient => Some(Self::from_at_gradient(node.into(), ctx)),
SyntaxKind::AtTr => Some(Self::from_at_tr(node.into(), ctx)),
SyntaxKind::QualifiedName => Some(Self::from_qualified_name_node(
node.clone().into(),
ctx,
LookupPhase::default(),
)),
SyntaxKind::FunctionCallExpression => {
Some(Self::from_function_call_node(node.into(), ctx))
}
SyntaxKind::MemberAccess => {
Some(Self::from_member_access_node(node.into(), ctx))
}
SyntaxKind::IndexExpression => {
Some(Self::from_index_expression_node(node.into(), ctx))
}
SyntaxKind::SelfAssignment => {
Some(Self::from_self_assignment_node(node.into(), ctx))
}
SyntaxKind::BinaryExpression => {
Some(Self::from_binary_expression_node(node.into(), ctx))
}
SyntaxKind::UnaryOpExpression => {
Some(Self::from_unaryop_expression_node(node.into(), ctx))
}
SyntaxKind::ConditionalExpression => {
Some(Self::from_conditional_expression_node(node.into(), ctx))
}
SyntaxKind::ObjectLiteral => {
Some(Self::from_object_literal_node(node.into(), ctx))
}
SyntaxKind::Array => Some(Self::from_array_node(node.into(), ctx)),
SyntaxKind::CodeBlock => Some(Self::from_codeblock_node(node.into(), ctx)),
SyntaxKind::StringTemplate => {
Some(Self::from_string_template_node(node.into(), ctx))
}
_ => None,
},
NodeOrToken::Token(token) => match token.kind() {
SyntaxKind::StringLiteral => Some(
crate::literals::unescape_string(token.text())
.map(Self::StringLiteral)
.unwrap_or_else(|| {
ctx.diag.push_error("Cannot parse string literal".into(), &token);
Self::Invalid
}),
),
SyntaxKind::NumberLiteral => Some(
crate::literals::parse_number_literal(token.text().into()).unwrap_or_else(
|e| {
ctx.diag.push_error(e.to_string(), &node);
Self::Invalid
},
),
),
SyntaxKind::ColorLiteral => Some(
crate::literals::parse_color_literal(token.text())
.map(|i| Expression::Cast {
from: Box::new(Expression::NumberLiteral(i as _, Unit::None)),
to: Type::Color,
})
.unwrap_or_else(|| {
ctx.diag.push_error("Invalid color literal".into(), &node);
Self::Invalid
}),
),
_ => None,
},
})
.unwrap_or(Self::Invalid)
}
fn from_at_image_url_node(node: syntax_nodes::AtImageUrl, ctx: &mut LookupCtx) -> Self {
let s = match node
.child_text(SyntaxKind::StringLiteral)
.and_then(|x| crate::literals::unescape_string(&x))
{
Some(s) => s,
None => {
ctx.diag.push_error("Cannot parse string literal".into(), &node);
return Self::Invalid;
}
};
if s.is_empty() {
return Expression::ImageReference {
resource_ref: ImageReference::None,
source_location: Some(node.to_source_location()),
nine_slice: None,
};
}
let absolute_source_path = {
let path = std::path::Path::new(&s);
if crate::pathutils::is_absolute(path) {
s
} else {
ctx.type_loader
.and_then(|loader| {
loader.resolve_import_path(Some(&(*node).clone().into()), &s)
})
.map(|i| i.0.to_string_lossy().into())
.unwrap_or_else(|| {
crate::pathutils::join(
&crate::pathutils::dirname(node.source_file.path()),
path,
)
.map(|p| p.to_string_lossy().into())
.unwrap_or(s.clone())
})
}
};
let nine_slice = node
.children_with_tokens()
.filter_map(|n| n.into_token())
.filter(|t| t.kind() == SyntaxKind::NumberLiteral)
.map(|arg| {
arg.text().parse().unwrap_or_else(|err: std::num::ParseIntError| {
match err.kind() {
IntErrorKind::PosOverflow | IntErrorKind::NegOverflow => {
ctx.diag.push_error("Number too big".into(), &arg)
}
IntErrorKind::InvalidDigit => ctx.diag.push_error(
"Border widths of a nine-slice can't have units".into(),
&arg,
),
_ => ctx.diag.push_error("Cannot parse number literal".into(), &arg),
};
0u16
})
})
.collect::<Vec<u16>>();
let nine_slice = match nine_slice.as_slice() {
[x] => Some([*x, *x, *x, *x]),
[x, y] => Some([*x, *y, *x, *y]),
[x, y, z, w] => Some([*x, *y, *z, *w]),
[] => None,
_ => {
assert!(ctx.diag.has_errors());
None
}
};
Expression::ImageReference {
resource_ref: ImageReference::AbsolutePath(absolute_source_path),
source_location: Some(node.to_source_location()),
nine_slice,
}
}
fn from_at_gradient(node: syntax_nodes::AtGradient, ctx: &mut LookupCtx) -> Self {
enum GradKind {
Linear { angle: Box<Expression> },
Radial,
}
let mut subs = node
.children_with_tokens()
.filter(|n| matches!(n.kind(), SyntaxKind::Comma | SyntaxKind::Expression));
let grad_token = node.child_token(SyntaxKind::Identifier).unwrap();
let grad_text = grad_token.text();
let grad_kind = if grad_text.starts_with("linear") {
let angle_expr = match subs.next() {
Some(e) if e.kind() == SyntaxKind::Expression => {
syntax_nodes::Expression::from(e.into_node().unwrap())
}
_ => {
ctx.diag.push_error("Expected angle expression".into(), &node);
return Expression::Invalid;
}
};
if subs.next().is_some_and(|s| s.kind() != SyntaxKind::Comma) {
ctx.diag.push_error(
"Angle expression must be an angle followed by a comma".into(),
&node,
);
return Expression::Invalid;
}
let angle = Box::new(
Expression::from_expression_node(angle_expr.clone(), ctx).maybe_convert_to(
Type::Angle,
&angle_expr,
ctx.diag,
),
);
GradKind::Linear { angle }
} else if grad_text.starts_with("radial") {
if !matches!(subs.next(), Some(NodeOrToken::Node(n)) if n.text().to_string().trim() == "circle")
{
ctx.diag.push_error("Expected 'circle': currently, only @radial-gradient(circle, ...) are supported".into(), &node);
return Expression::Invalid;
}
let comma = subs.next();
if matches!(&comma, Some(NodeOrToken::Node(n)) if n.text().to_string().trim() == "at") {
ctx.diag.push_error("'at' in @radial-gradient is not yet supported".into(), &comma);
return Expression::Invalid;
}
if comma.as_ref().is_some_and(|s| s.kind() != SyntaxKind::Comma) {
ctx.diag.push_error(
"'circle' must be followed by a comma".into(),
comma.as_ref().map_or(&node, |x| x as &dyn Spanned),
);
return Expression::Invalid;
}
GradKind::Radial
} else {
// Parser should have ensured we have one of the linear or radial gradient
panic!("Not a gradient {grad_text:?}");
};
let mut stops = vec![];
enum Stop {
Empty,
Color(Expression),
Finished,
}
let mut current_stop = Stop::Empty;
for n in subs {
if n.kind() == SyntaxKind::Comma {
match std::mem::replace(&mut current_stop, Stop::Empty) {
Stop::Empty => {
ctx.diag.push_error("Expected expression".into(), &n);
break;
}
Stop::Finished => {}
Stop::Color(col) => stops.push((
col,
if stops.is_empty() {
Expression::NumberLiteral(0., Unit::None)
} else {
Expression::Invalid
},
)),
}
} else {
// To facilitate color literal conversion, adjust the expected return type.
let e = {
let old_property_type = std::mem::replace(&mut ctx.property_type, Type::Color);
let e =
Expression::from_expression_node(n.as_node().unwrap().clone().into(), ctx);
ctx.property_type = old_property_type;
e
};
match std::mem::replace(&mut current_stop, Stop::Finished) {
Stop::Empty => {
current_stop = Stop::Color(e.maybe_convert_to(Type::Color, &n, ctx.diag))
}
Stop::Finished => {
ctx.diag.push_error("Expected comma".into(), &n);
break;
}
Stop::Color(col) => {
stops.push((col, e.maybe_convert_to(Type::Float32, &n, ctx.diag)))
}
}
}
}
match current_stop {
Stop::Color(col) => stops.push((col, Expression::NumberLiteral(1., Unit::None))),
Stop::Empty => {
if let Some((_, e @ Expression::Invalid)) = stops.last_mut() {
*e = Expression::NumberLiteral(1., Unit::None)
}
}
Stop::Finished => (),
};
// Fix the stop so each has a position.
let mut start = 0;
while start < stops.len() {
start += match stops[start..].iter().position(|s| matches!(s.1, Expression::Invalid)) {
Some(p) => p,
None => break,
};
let (before, rest) = stops.split_at_mut(start);
let pos =
rest.iter().position(|s| !matches!(s.1, Expression::Invalid)).unwrap_or(rest.len());
if pos > 0 && pos < rest.len() {
let (middle, after) = rest.split_at_mut(pos);
let begin = before
.last()
.map(|s| &s.1)
.unwrap_or(&Expression::NumberLiteral(1., Unit::None));
let end = &after.first().expect("The last should never be invalid").1;
for (i, (_, e)) in middle.iter_mut().enumerate() {
debug_assert!(matches!(e, Expression::Invalid));
// e = begin + (i+1) * (end - begin) / (pos+1)
*e = Expression::BinaryExpression {
lhs: Box::new(begin.clone()),
rhs: Box::new(Expression::BinaryExpression {
lhs: Box::new(Expression::BinaryExpression {
lhs: Box::new(Expression::NumberLiteral(i as f64 + 1., Unit::None)),
rhs: Box::new(Expression::BinaryExpression {
lhs: Box::new(end.clone()),
rhs: Box::new(begin.clone()),
op: '-',
}),
op: '*',
}),
rhs: Box::new(Expression::NumberLiteral(pos as f64 + 1., Unit::None)),
op: '/',
}),
op: '+',
};
}
}
start += pos + 1;
}
match grad_kind {
GradKind::Linear { angle } => Expression::LinearGradient { angle, stops },
GradKind::Radial => Expression::RadialGradient { stops },
}
}
fn from_at_tr(node: syntax_nodes::AtTr, ctx: &mut LookupCtx) -> Expression {
let Some(string) = node
.child_text(SyntaxKind::StringLiteral)
.and_then(|s| crate::literals::unescape_string(&s))
else {
ctx.diag.push_error("Cannot parse string literal".into(), &node);
return Expression::Invalid;
};
let context = node.TrContext().map(|n| {
n.child_text(SyntaxKind::StringLiteral)
.and_then(|s| crate::literals::unescape_string(&s))
.unwrap_or_else(|| {
ctx.diag.push_error("Cannot parse string literal".into(), &n);
Default::default()
})
});
let plural = node.TrPlural().map(|pl| {
let s = pl
.child_text(SyntaxKind::StringLiteral)
.and_then(|s| crate::literals::unescape_string(&s))
.unwrap_or_else(|| {
ctx.diag.push_error("Cannot parse string literal".into(), &pl);
Default::default()
});
let n = pl.Expression();
let expr = Expression::from_expression_node(n.clone(), ctx).maybe_convert_to(
Type::Int32,
&n,
ctx.diag,
);
(s, expr)
});
let domain = ctx
.type_loader
.and_then(|tl| tl.compiler_config.translation_domain.clone())
.unwrap_or_default();
let subs = node.Expression().map(|n| {
Expression::from_expression_node(n.clone(), ctx).maybe_convert_to(
Type::String,
&n,
ctx.diag,
)
});
let values = subs.collect::<Vec<_>>();
// check format string
{
let mut arg_idx = 0;
let mut pos_max = 0;
let mut pos = 0;
let mut has_n = false;
while let Some(mut p) = string[pos..].find(['{', '}']) {
if string.len() - pos < p + 1 {
ctx.diag.push_error(
"Unescaped trailing '{' in format string. Escape '{' with '{{'".into(),
&node,
);
break;
}
p += pos;
// Skip escaped }
if string.get(p..=p) == Some("}") {
if string.get(p + 1..=p + 1) == Some("}") {
pos = p + 2;
continue;
} else {
ctx.diag.push_error(
"Unescaped '}' in format string. Escape '}' with '}}'".into(),
&node,
);
break;
}
}
// Skip escaped {
if string.get(p + 1..=p + 1) == Some("{") {
pos = p + 2;
continue;
}
// Find the argument
let end = if let Some(end) = string[p..].find('}') {
end + p
} else {
ctx.diag.push_error(
"Unterminated placeholder in format string. '{' must be escaped with '{{'"
.into(),
&node,
);
break;
};
let argument = &string[p + 1..end];
if argument.is_empty() {
arg_idx += 1;
} else if let Ok(n) = argument.parse::<u16>() {
pos_max = pos_max.max(n as usize + 1);
} else if argument == "n" {
has_n = true;
if plural.is_none() {
ctx.diag.push_error(
"`{n}` placeholder can only be found in plural form".into(),
&node,
);
}
} else {
ctx.diag
.push_error("Invalid '{...}' placeholder in format string. The placeholder must be a number, or braces must be escaped with '{{' and '}}'".into(), &node);
break;
};
pos = end + 1;
}
if arg_idx > 0 && pos_max > 0 {
ctx.diag.push_error(
"Cannot mix positional and non-positional placeholder in format string".into(),
&node,
);
} else if arg_idx > values.len() || pos_max > values.len() {
let num = arg_idx.max(pos_max);
let note = if !has_n && plural.is_some() {
". Note: use `{n}` for the argument after '%'"
} else {
""
};
ctx.diag.push_error(
format!("Format string contains {num} placeholders, but only {} extra arguments were given{note}", values.len()),
&node,
);
}
}
let plural =
plural.unwrap_or((SmolStr::default(), Expression::NumberLiteral(1., Unit::None)));
let get_component_name = || {
ctx.component_scope
.first()
.and_then(|e| e.borrow().enclosing_component.upgrade())
.map(|c| c.id.clone())
};
Expression::FunctionCall {
function: BuiltinFunction::Translate.into(),
arguments: vec![
Expression::StringLiteral(string),
Expression::StringLiteral(context.or_else(get_component_name).unwrap_or_default()),
Expression::StringLiteral(domain.into()),
Expression::Array { element_ty: Type::String, values },
plural.1,
Expression::StringLiteral(plural.0),
],
source_location: Some(node.to_source_location()),
}
}
/// Perform the lookup
fn from_qualified_name_node(
node: syntax_nodes::QualifiedName,
ctx: &mut LookupCtx,
phase: LookupPhase,
) -> Self {
Self::from_lookup_result(lookup_qualified_name_node(node.clone(), ctx, phase), ctx, &node)
}
fn from_lookup_result(
r: Option<LookupResult>,
ctx: &mut LookupCtx,
node: &dyn Spanned,
) -> Self {
let Some(r) = r else {
assert!(ctx.diag.has_errors());
return Self::Invalid;
};
match r {
LookupResult::Expression { expression, .. } => expression,
LookupResult::Callable(c) => {
let what = match c {
LookupResultCallable::Callable(Callable::Callback(..)) => "Callback",
LookupResultCallable::Callable(Callable::Builtin(..)) => "Builtin function",
LookupResultCallable::Macro(..) => "Builtin function",
LookupResultCallable::MemberFunction { .. } => "Member function",
_ => "Function",
};
ctx.diag
.push_error(format!("{what} must be called. Did you forgot the '()'?",), node);
Self::Invalid
}
LookupResult::Enumeration(..) => {
ctx.diag.push_error("Cannot take reference to an enum".to_string(), node);
Self::Invalid
}
LookupResult::Namespace(..) => {
ctx.diag.push_error("Cannot take reference to a namespace".to_string(), node);
Self::Invalid
}
}
}
fn from_function_call_node(
node: syntax_nodes::FunctionCallExpression,
ctx: &mut LookupCtx,
) -> Expression {
let mut arguments = Vec::new();
let mut sub_expr = node.Expression();
let func_expr = sub_expr.next().unwrap();
let (function, source_location) = if let Some(qn) = func_expr.QualifiedName() {
let sl = qn.last_token().unwrap().to_source_location();
(lookup_qualified_name_node(qn, ctx, LookupPhase::default()), sl)
} else if let Some(ma) = func_expr.MemberAccess() {
let base = Self::from_expression_node(ma.Expression(), ctx);
let field = ma.child_token(SyntaxKind::Identifier);
let sl = field.to_source_location();
(maybe_lookup_object(base.into(), field.clone().into_iter(), ctx), sl)
} else {
if Self::from_expression_node(func_expr, ctx).ty() == Type::Invalid {
assert!(ctx.diag.has_errors());
} else {
ctx.diag.push_error("The expression is not a function".into(), &node);
}
return Self::Invalid;
};
let sub_expr = sub_expr.map(|n| {
(Self::from_expression_node(n.clone(), ctx), Some(NodeOrToken::from((*n).clone())))
});
let Some(function) = function else {
// Check sub expressions anyway
sub_expr.count();
assert!(ctx.diag.has_errors());
return Self::Invalid;
};
let LookupResult::Callable(function) = function else {
// Check sub expressions anyway
sub_expr.count();
ctx.diag.push_error("The expression is not a function".into(), &node);
return Self::Invalid;
};
let mut adjust_arg_count = 0;
let function = match function {
LookupResultCallable::Callable(c) => c,
LookupResultCallable::Macro(mac) => {
arguments.extend(sub_expr);
return crate::builtin_macros::lower_macro(
mac,
&source_location,
arguments.into_iter(),
ctx.diag,
);
}
LookupResultCallable::MemberFunction { member, base, base_node } => {
arguments.push((base, base_node));
adjust_arg_count = 1;
match *member {
LookupResultCallable::Callable(c) => c,
LookupResultCallable::Macro(mac) => {
arguments.extend(sub_expr);
return crate::builtin_macros::lower_macro(
mac,
&source_location,
arguments.into_iter(),
ctx.diag,
);
}
LookupResultCallable::MemberFunction { .. } => {
unreachable!()
}
}
}
};
arguments.extend(sub_expr);
let arguments = match function.ty() {
Type::Function(function) | Type::Callback(function) => {
if arguments.len() != function.args.len() {
ctx.diag.push_error(
format!(
"The callback or function expects {} arguments, but {} are provided",
function.args.len() - adjust_arg_count,
arguments.len() - adjust_arg_count,
),
&node,
);
arguments.into_iter().map(|x| x.0).collect()
} else {
arguments
.into_iter()
.zip(function.args.iter())
.map(|((e, node), ty)| e.maybe_convert_to(ty.clone(), &node, ctx.diag))
.collect()
}
}
Type::Invalid => {
debug_assert!(ctx.diag.has_errors(), "The error must already have been reported.");
arguments.into_iter().map(|x| x.0).collect()
}
_ => {
ctx.diag.push_error("The expression is not a function".into(), &node);
arguments.into_iter().map(|x| x.0).collect()
}
};
Expression::FunctionCall { function, arguments, source_location: Some(source_location) }
}
fn from_member_access_node(
node: syntax_nodes::MemberAccess,
ctx: &mut LookupCtx,
) -> Expression {
let base = Self::from_expression_node(node.Expression(), ctx);
let field = node.child_token(SyntaxKind::Identifier);
Self::from_lookup_result(
maybe_lookup_object(base.into(), field.clone().into_iter(), ctx),
ctx,
&field,
)
}
fn from_self_assignment_node(
node: syntax_nodes::SelfAssignment,
ctx: &mut LookupCtx,
) -> Expression {
let (lhs_n, rhs_n) = node.Expression();
let mut lhs = Self::from_expression_node(lhs_n.clone(), ctx);
let op = node
.children_with_tokens()
.find_map(|n| match n.kind() {
SyntaxKind::PlusEqual => Some('+'),
SyntaxKind::MinusEqual => Some('-'),
SyntaxKind::StarEqual => Some('*'),
SyntaxKind::DivEqual => Some('/'),
SyntaxKind::Equal => Some('='),
_ => None,
})
.unwrap_or('_');
if lhs.ty() != Type::Invalid {
lhs.try_set_rw(ctx, if op == '=' { "Assignment" } else { "Self assignment" }, &node);
}
let ty = lhs.ty();
let expected_ty = match op {
'=' => ty,
'+' if ty == Type::String || ty.as_unit_product().is_some() => ty,
'-' if ty.as_unit_product().is_some() => ty,
'/' | '*' if ty.as_unit_product().is_some() => Type::Float32,
_ => {
if ty != Type::Invalid {
ctx.diag.push_error(
format!("the {op}= operation cannot be done on a {ty}"),
&lhs_n,
);
}
Type::Invalid
}
};
let rhs = Self::from_expression_node(rhs_n.clone(), ctx);
Expression::SelfAssignment {
lhs: Box::new(lhs),
rhs: Box::new(rhs.maybe_convert_to(expected_ty, &rhs_n, ctx.diag)),
op,
node: Some(NodeOrToken::Node(node.into())),
}
}
fn from_binary_expression_node(
node: syntax_nodes::BinaryExpression,
ctx: &mut LookupCtx,
) -> Expression {
let op = node
.children_with_tokens()
.find_map(|n| match n.kind() {
SyntaxKind::Plus => Some('+'),
SyntaxKind::Minus => Some('-'),
SyntaxKind::Star => Some('*'),
SyntaxKind::Div => Some('/'),
SyntaxKind::LessEqual => Some('≤'),
SyntaxKind::GreaterEqual => Some('≥'),
SyntaxKind::LAngle => Some('<'),
SyntaxKind::RAngle => Some('>'),
SyntaxKind::EqualEqual => Some('='),
SyntaxKind::NotEqual => Some('!'),
SyntaxKind::AndAnd => Some('&'),
SyntaxKind::OrOr => Some('|'),
_ => None,
})
.unwrap_or('_');
let (lhs_n, rhs_n) = node.Expression();
let lhs = Self::from_expression_node(lhs_n.clone(), ctx);
let rhs = Self::from_expression_node(rhs_n.clone(), ctx);
let expected_ty = match operator_class(op) {
OperatorClass::ComparisonOp => {
Self::common_target_type_for_type_list([lhs.ty(), rhs.ty()].iter().cloned())
}
OperatorClass::LogicalOp => Type::Bool,
OperatorClass::ArithmeticOp => {
let (lhs_ty, rhs_ty) = (lhs.ty(), rhs.ty());
if op == '+' && (lhs_ty == Type::String || rhs_ty == Type::String) {
Type::String
} else if op == '+' || op == '-' {
if lhs_ty.default_unit().is_some() {
lhs_ty
} else if rhs_ty.default_unit().is_some() {
rhs_ty
} else if matches!(lhs_ty, Type::UnitProduct(_)) {
lhs_ty
} else if matches!(rhs_ty, Type::UnitProduct(_)) {
rhs_ty
} else {
Type::Float32
}
} else if op == '*' || op == '/' {
let has_unit = |ty: &Type| {
matches!(ty, Type::UnitProduct(_)) || ty.default_unit().is_some()
};
match (has_unit(&lhs_ty), has_unit(&rhs_ty)) {
(true, true) => {
return Expression::BinaryExpression {
lhs: Box::new(lhs),
rhs: Box::new(rhs),
op,
}
}
(true, false) => {
return Expression::BinaryExpression {
lhs: Box::new(lhs),
rhs: Box::new(rhs.maybe_convert_to(
Type::Float32,
&rhs_n,
ctx.diag,
)),
op,
}
}
(false, true) => {
return Expression::BinaryExpression {
lhs: Box::new(lhs.maybe_convert_to(
Type::Float32,
&lhs_n,
ctx.diag,
)),
rhs: Box::new(rhs),
op,
}
}
(false, false) => Type::Float32,
}
} else {
unreachable!()
}
}
};
Expression::BinaryExpression {
lhs: Box::new(lhs.maybe_convert_to(expected_ty.clone(), &lhs_n, ctx.diag)),
rhs: Box::new(rhs.maybe_convert_to(expected_ty, &rhs_n, ctx.diag)),
op,
}
}
fn from_unaryop_expression_node(
node: syntax_nodes::UnaryOpExpression,
ctx: &mut LookupCtx,
) -> Expression {
let exp_n = node.Expression();
let exp = Self::from_expression_node(exp_n, ctx);
let op = node
.children_with_tokens()
.find_map(|n| match n.kind() {
SyntaxKind::Plus => Some('+'),
SyntaxKind::Minus => Some('-'),
SyntaxKind::Bang => Some('!'),
_ => None,
})
.unwrap_or('_');
let exp = match op {
'!' => exp.maybe_convert_to(Type::Bool, &node, ctx.diag),
'+' | '-' => {
let ty = exp.ty();
if ty.default_unit().is_none()
&& !matches!(
ty,
Type::Int32
| Type::Float32
| Type::Percent
| Type::UnitProduct(..)
| Type::Invalid
)
{
ctx.diag.push_error(format!("Unary '{op}' not supported on {ty}"), &node);
}
exp
}
_ => {
assert!(ctx.diag.has_errors());
exp
}
};
Expression::UnaryOp { sub: Box::new(exp), op }
}
fn from_conditional_expression_node(
node: syntax_nodes::ConditionalExpression,
ctx: &mut LookupCtx,
) -> Expression {
let (condition_n, true_expr_n, false_expr_n) = node.Expression();
// FIXME: we should we add bool to the context
let condition = Self::from_expression_node(condition_n.clone(), ctx).maybe_convert_to(
Type::Bool,
&condition_n,
ctx.diag,
);
let true_expr = Self::from_expression_node(true_expr_n.clone(), ctx);
let false_expr = Self::from_expression_node(false_expr_n.clone(), ctx);
let result_ty = Self::common_target_type_for_type_list(
[true_expr.ty(), false_expr.ty()].iter().cloned(),
);
let true_expr = true_expr.maybe_convert_to(result_ty.clone(), &true_expr_n, ctx.diag);
let false_expr = false_expr.maybe_convert_to(result_ty, &false_expr_n, ctx.diag);
Expression::Condition {
condition: Box::new(condition),
true_expr: Box::new(true_expr),
false_expr: Box::new(false_expr),
}
}
fn from_index_expression_node(
node: syntax_nodes::IndexExpression,
ctx: &mut LookupCtx,
) -> Expression {
let (array_expr_n, index_expr_n) = node.Expression();
let array_expr = Self::from_expression_node(array_expr_n, ctx);
let index_expr = Self::from_expression_node(index_expr_n.clone(), ctx).maybe_convert_to(
Type::Int32,
&index_expr_n,
ctx.diag,
);
let ty = array_expr.ty();
if !matches!(ty, Type::Array(_) | Type::Invalid | Type::Function(_) | Type::Callback(_)) {
ctx.diag.push_error(format!("{ty} is not an indexable type"), &node);
}
Expression::ArrayIndex { array: Box::new(array_expr), index: Box::new(index_expr) }
}
fn from_object_literal_node(
node: syntax_nodes::ObjectLiteral,
ctx: &mut LookupCtx,
) -> Expression {
let values: HashMap<SmolStr, Expression> = node
.ObjectMember()
.map(|n| {
(
identifier_text(&n).unwrap_or_default(),
Expression::from_expression_node(n.Expression(), ctx),
)
})
.collect();
let ty = Rc::new(Struct {
fields: values.iter().map(|(k, v)| (k.clone(), v.ty())).collect(),
name: None,
node: None,
rust_attributes: None,
});
Expression::Struct { ty, values }
}
fn from_array_node(node: syntax_nodes::Array, ctx: &mut LookupCtx) -> Expression {
let mut values: Vec<Expression> =
node.Expression().map(|e| Expression::from_expression_node(e, ctx)).collect();
let element_ty = if values.is_empty() {
Type::Void
} else {
Self::common_target_type_for_type_list(values.iter().map(|expr| expr.ty()))
};
for e in values.iter_mut() {
*e = core::mem::replace(e, Expression::Invalid).maybe_convert_to(
element_ty.clone(),
&node,
ctx.diag,
);
}
Expression::Array { element_ty, values }
}
fn from_string_template_node(
node: syntax_nodes::StringTemplate,
ctx: &mut LookupCtx,
) -> Expression {
let mut exprs = node.Expression().map(|e| {
Expression::from_expression_node(e.clone(), ctx).maybe_convert_to(
Type::String,
&e,
ctx.diag,
)
});
let mut result = exprs.next().unwrap_or_default();
for x in exprs {
result = Expression::BinaryExpression {
lhs: Box::new(std::mem::take(&mut result)),
rhs: Box::new(x),
op: '+',
}
}
result
}
/// This function is used to find a type that's suitable for casting each instance of a bunch of expressions
/// to a type that captures most aspects. For example for an array of object literals the result is a merge of
/// all seen fields.
pub fn common_target_type_for_type_list(types: impl Iterator<Item = Type>) -> Type {
types.fold(Type::Invalid, |target_type, expr_ty| {
if target_type == expr_ty {
target_type
} else if target_type == Type::Invalid {
expr_ty
} else {
match (target_type, expr_ty) {
(Type::Struct(ref result), Type::Struct(ref elem)) => {
let mut fields = result.fields.clone();
for (elem_name, elem_ty) in elem.fields.iter() {
match fields.entry(elem_name.clone()) {
std::collections::btree_map::Entry::Vacant(free_entry) => {
free_entry.insert(elem_ty.clone());
}
std::collections::btree_map::Entry::Occupied(
mut existing_field,
) => {
*existing_field.get_mut() =
Self::common_target_type_for_type_list(
[existing_field.get().clone(), elem_ty.clone()]
.into_iter(),
);
}
}
}
Type::Struct(Rc::new(Struct {
name: result.name.as_ref().or(elem.name.as_ref()).cloned(),
fields,
node: result.node.as_ref().or(elem.node.as_ref()).cloned(),
rust_attributes: result
.rust_attributes
.as_ref()
.or(elem.rust_attributes.as_ref())
.cloned(),
}))
}
(Type::Array(lhs), Type::Array(rhs)) => Type::Array(if *lhs == Type::Void {
rhs
} else if *rhs == Type::Void {
lhs
} else {
Self::common_target_type_for_type_list(
[(*lhs).clone(), (*rhs).clone()].into_iter(),
)
.into()
}),
(Type::Color, Type::Brush) | (Type::Brush, Type::Color) => Type::Brush,
(target_type, expr_ty) => {
if expr_ty.can_convert(&target_type) {
target_type
} else if target_type.can_convert(&expr_ty)
|| (expr_ty.default_unit().is_some()
&& matches!(target_type, Type::Float32 | Type::Int32))
{
// in the or case: The `0` literal.
expr_ty
} else {
// otherwise, use the target type and let further conversion report an error
target_type
}
}
}
}
})
}
}
/// Perform the lookup
fn lookup_qualified_name_node(
node: syntax_nodes::QualifiedName,
ctx: &mut LookupCtx,
phase: LookupPhase,
) -> Option<LookupResult> {
let mut it = node
.children_with_tokens()
.filter(|n| n.kind() == SyntaxKind::Identifier)
.filter_map(|n| n.into_token());
let first = if let Some(first) = it.next() {
first
} else {
// There must be at least one member (parser should ensure that)
debug_assert!(ctx.diag.has_errors());
return None;
};
ctx.current_token = Some(first.clone().into());
let first_str = crate::parser::normalize_identifier(first.text());
let global_lookup = crate::lookup::global_lookup();
let result = match global_lookup.lookup(ctx, &first_str) {
None => {
if let Some(minus_pos) = first.text().find('-') {
// Attempt to recover if the user wanted to write "-" for minus
let first_str = &first.text()[0..minus_pos];
if global_lookup
.lookup(ctx, &crate::parser::normalize_identifier(first_str))
.is_some()
{
ctx.diag.push_error(format!("Unknown unqualified identifier '{}'. Use space before the '-' if you meant a subtraction", first.text()), &node);
return None;
}
}
for (prefix, e) in
[("self", ctx.component_scope.last()), ("root", ctx.component_scope.first())]
{
if let Some(e) = e {
if e.lookup(ctx, &first_str).is_some() {
ctx.diag.push_error(format!("Unknown unqualified identifier '{0}'. Did you mean '{prefix}.{0}'?", first.text()), &node);
return None;
}
}
}
if it.next().is_some() {
ctx.diag.push_error(format!("Cannot access id '{}'", first.text()), &node);
} else {
ctx.diag.push_error(
format!("Unknown unqualified identifier '{}'", first.text()),
&node,
);
}
return None;
}
Some(x) => x,
};
if let Some(depr) = result.deprecated() {
ctx.diag.push_property_deprecation_warning(&first_str, depr, &first);
}
match result {
LookupResult::Expression { expression: Expression::ElementReference(e), .. } => {
continue_lookup_within_element(&e.upgrade().unwrap(), &mut it, node, ctx)
}
LookupResult::Expression {
expression: Expression::RepeaterModelReference { .. }, ..
} if matches!(phase, LookupPhase::ResolvingTwoWayBindings) => {
ctx.diag.push_error(
"Two-way bindings to model data is not supported yet".to_string(),
&node,
);
None
}
result => maybe_lookup_object(result, it, ctx),
}
}
fn continue_lookup_within_element(
elem: &ElementRc,
it: &mut impl Iterator<Item = crate::parser::SyntaxToken>,
node: syntax_nodes::QualifiedName,
ctx: &mut LookupCtx,
) -> Option<LookupResult> {
let second = if let Some(second) = it.next() {
second
} else if matches!(ctx.property_type, Type::ElementReference) {
return Some(Expression::ElementReference(Rc::downgrade(elem)).into());
} else {
// Try to recover in case we wanted to access a property
let mut rest = String::new();
if let Some(LookupResult::Expression {
expression: Expression::PropertyReference(nr),
..
}) = crate::lookup::InScopeLookup.lookup(ctx, &elem.borrow().id)
{
let e = nr.element();
let e_borrowed = e.borrow();
let mut id = e_borrowed.id.as_str();
if id.is_empty() {
if ctx.component_scope.last().is_some_and(|x| Rc::ptr_eq(&e, x)) {
id = "self";
} else if ctx.component_scope.first().is_some_and(|x| Rc::ptr_eq(&e, x)) {
id = "root";
} else if ctx.component_scope.iter().nth_back(1).is_some_and(|x| Rc::ptr_eq(&e, x))
{
id = "parent";
}
};
if !id.is_empty() {
rest =
format!(". Use '{id}.{}' to access the property with the same name", nr.name());
}
} else if let Some(LookupResult::Expression {
expression: Expression::EnumerationValue(value),
..
}) = crate::lookup::ReturnTypeSpecificLookup.lookup(ctx, &elem.borrow().id)
{
rest = format!(
". Use '{}.{value}' to access the enumeration value",
value.enumeration.name
);
}
ctx.diag.push_error(format!("Cannot take reference of an element{rest}"), &node);
return None;
};
let prop_name = crate::parser::normalize_identifier(second.text());
let lookup_result = elem.borrow().lookup_property(&prop_name);
let local_to_component = lookup_result.is_local_to_component && ctx.is_local_element(elem);
if lookup_result.property_type.is_property_type() {
if !local_to_component && lookup_result.property_visibility == PropertyVisibility::Private {
ctx.diag.push_error(format!("The property '{}' is private. Annotate it with 'in', 'out' or 'in-out' to make it accessible from other components", second.text()), &second);
return None;
} else if lookup_result.property_visibility == PropertyVisibility::Fake {
ctx.diag.push_error(
"This special property can only be used to make a binding and cannot be accessed"
.to_string(),
&second,
);
return None;
} else if lookup_result.resolved_name != prop_name.as_str() {
ctx.diag.push_property_deprecation_warning(
&prop_name,
&lookup_result.resolved_name,
&second,
);
} else if let Some(deprecated) =
crate::lookup::check_deprecated_stylemetrics(elem, ctx, &prop_name)
{
ctx.diag.push_property_deprecation_warning(&prop_name, &deprecated, &second);
}
let prop = Expression::PropertyReference(NamedReference::new(
elem,
lookup_result.resolved_name.to_smolstr(),
));
maybe_lookup_object(prop.into(), it, ctx)
} else if matches!(lookup_result.property_type, Type::Callback { .. }) {
if let Some(x) = it.next() {
ctx.diag.push_error("Cannot access fields of callback".into(), &x)
}
Some(LookupResult::Callable(LookupResultCallable::Callable(Callable::Callback(
NamedReference::new(elem, lookup_result.resolved_name.to_smolstr()),
))))
} else if matches!(lookup_result.property_type, Type::Function { .. }) {
if lookup_result.property_visibility == PropertyVisibility::Private && !local_to_component {
let message = format!("The function '{}' is private. Annotate it with 'public' to make it accessible from other components", second.text());
if !lookup_result.is_local_to_component {
ctx.diag.push_error(message, &second);
} else {
ctx.diag.push_warning(message+". Note: this used to be allowed in previous version, but this should be considered an error", &second);
}
} else if lookup_result.property_visibility == PropertyVisibility::Protected
&& !local_to_component
&& !(lookup_result.is_in_direct_base
&& ctx.component_scope.first().is_some_and(|x| Rc::ptr_eq(x, elem)))
{
ctx.diag.push_error(format!("The function '{}' is protected", second.text()), &second);
}
if let Some(x) = it.next() {
ctx.diag.push_error("Cannot access fields of a function".into(), &x)
}
if let Some(f) =
elem.borrow().base_type.lookup_member_function(&lookup_result.resolved_name)
{
// builtin member function
LookupResult::Callable(LookupResultCallable::MemberFunction {
base: Expression::ElementReference(Rc::downgrade(elem)),
base_node: Some(NodeOrToken::Node(node.into())),
member: Box::new(LookupResultCallable::Callable(f.into())),
})
.into()
} else {
LookupResult::from(Callable::Function(NamedReference::new(
elem,
lookup_result.resolved_name.to_smolstr(),
)))
.into()
}
} else {
let mut err = |extra: &str| {
let what = match &elem.borrow().base_type {
ElementType::Global => {
let global = elem.borrow().enclosing_component.upgrade().unwrap();
assert!(global.is_global());
format!("'{}'", global.id)
}
ElementType::Component(c) => format!("Element '{}'", c.id),
ElementType::Builtin(b) => format!("Element '{}'", b.name),
ElementType::Native(_) => unreachable!("the native pass comes later"),
ElementType::Error => {
assert!(ctx.diag.has_errors());
return;
}
};
ctx.diag.push_error(
format!("{} does not have a property '{}'{}", what, second.text(), extra),
&second,
);
};
if let Some(minus_pos) = second.text().find('-') {
// Attempt to recover if the user wanted to write "-"
if elem
.borrow()
.lookup_property(&crate::parser::normalize_identifier(&second.text()[0..minus_pos]))
.property_type
!= Type::Invalid
{
err(". Use space before the '-' if you meant a subtraction");
return None;
}
}
err("");
None
}
}
fn maybe_lookup_object(
mut base: LookupResult,
it: impl Iterator<Item = crate::parser::SyntaxToken>,
ctx: &mut LookupCtx,
) -> Option<LookupResult> {
for next in it {
let next_str = crate::parser::normalize_identifier(next.text());
ctx.current_token = Some(next.clone().into());
match base.lookup(ctx, &next_str) {
Some(r) => {
base = r;
}
None => {
if let Some(minus_pos) = next.text().find('-') {
if base.lookup(ctx, &SmolStr::new(&next.text()[0..minus_pos])).is_some() {
ctx.diag.push_error(format!("Cannot access the field '{}'. Use space before the '-' if you meant a subtraction", next.text()), &next);
return None;
}
}
match base {
LookupResult::Callable(LookupResultCallable::Callable(Callable::Callback(
..,
))) => ctx.diag.push_error("Cannot access fields of callback".into(), &next),
LookupResult::Callable(..) => {
ctx.diag.push_error("Cannot access fields of a function".into(), &next)
}
LookupResult::Enumeration(enumeration) => ctx.diag.push_error(
format!(
"'{}' is not a member of the enum {}",
next.text(),
enumeration.name
),
&next,
),
LookupResult::Namespace(ns) => {
ctx.diag.push_error(
format!("'{}' is not a member of the namespace {}", next.text(), ns),
&next,
);
}
LookupResult::Expression { expression, .. } => {
let ty_descr = match expression.ty() {
Type::Struct { .. } => String::new(),
ty => format!(" of {ty}"),
};
ctx.diag.push_error(
format!("Cannot access the field '{}'{}", next.text(), ty_descr),
&next,
);
}
}
return None;
}
}
}
Some(base)
}
/// Go through all the two way binding and resolve them first
fn resolve_two_way_bindings(
doc: &Document,
type_register: &TypeRegister,
diag: &mut BuildDiagnostics,
) {
for component in doc.inner_components.iter() {
recurse_elem_with_scope(
&component.root_element,
ComponentScope(vec![]),
&mut |elem, scope| {
for (prop_name, binding) in &elem.borrow().bindings {
let mut binding = binding.borrow_mut();
if let Expression::Uncompiled(node) = binding.expression.clone() {
if let Some(n) = syntax_nodes::TwoWayBinding::new(node.clone()) {
let lhs_lookup = elem.borrow().lookup_property(prop_name);
if !lhs_lookup.is_valid() {
// An attempt to resolve this already failed when trying to resolve the property type
assert!(diag.has_errors());
continue;
}
let mut lookup_ctx = LookupCtx {
property_name: Some(prop_name.as_str()),
property_type: lhs_lookup.property_type.clone(),
component_scope: &scope.0,
diag,
arguments: vec![],
type_register,
type_loader: None,
current_token: Some(node.clone().into()),
};
binding.expression = Expression::Invalid;
if let Some(nr) = resolve_two_way_binding(n, &mut lookup_ctx) {
binding.two_way_bindings.push(nr.clone());
nr.element()
.borrow()
.property_analysis
.borrow_mut()
.entry(nr.name().clone())
.or_default()
.is_linked = true;
if matches!(
lhs_lookup.property_visibility,
PropertyVisibility::Private | PropertyVisibility::Output
) && !lhs_lookup.is_local_to_component
{
// invalid property assignment should have been reported earlier
assert!(diag.has_errors() || elem.borrow().is_legacy_syntax);
continue;
}
// Check the compatibility.
let mut rhs_lookup =
nr.element().borrow().lookup_property(nr.name());
if rhs_lookup.property_type == Type::Invalid {
// An attempt to resolve this already failed when trying to resolve the property type
assert!(diag.has_errors());
continue;
}
rhs_lookup.is_local_to_component &=
lookup_ctx.is_local_element(&nr.element());
if !rhs_lookup.is_valid_for_assignment() {
match (
lhs_lookup.property_visibility,
rhs_lookup.property_visibility,
) {
(PropertyVisibility::Input, PropertyVisibility::Input)
if !lhs_lookup.is_local_to_component =>
{
assert!(rhs_lookup.is_local_to_component);
marked_linked_read_only(elem, prop_name);
}
(
PropertyVisibility::Output
| PropertyVisibility::Private,
PropertyVisibility::Output | PropertyVisibility::Input,
) => {
assert!(lhs_lookup.is_local_to_component);
marked_linked_read_only(elem, prop_name);
}
(PropertyVisibility::Input, PropertyVisibility::Output)
if !lhs_lookup.is_local_to_component =>
{
assert!(!rhs_lookup.is_local_to_component);
marked_linked_read_only(elem, prop_name);
}
_ => {
if lookup_ctx.is_legacy_component() {
diag.push_warning(
format!(
"Link to a {} property is deprecated",
rhs_lookup.property_visibility
),
&node,
);
} else {
diag.push_error(
format!(
"Cannot link to a {} property",
rhs_lookup.property_visibility
),
&node,
)
}
}
}
} else if !lhs_lookup.is_valid_for_assignment() {
if rhs_lookup.is_local_to_component
&& rhs_lookup.property_visibility
== PropertyVisibility::InOut
{
if lookup_ctx.is_legacy_component() {
debug_assert!(!diag.is_empty()); // warning should already be reported
} else {
diag.push_error(
"Cannot link input property".into(),
&node,
);
}
} else if rhs_lookup.property_visibility
== PropertyVisibility::InOut
{
diag.push_warning("Linking input properties to input output properties is deprecated".into(), &node);
marked_linked_read_only(&nr.element(), nr.name());
} else {
// This is allowed, but then the rhs must also become read only.
marked_linked_read_only(&nr.element(), nr.name());
}
}
}
}
}
}
},
);
}
fn marked_linked_read_only(elem: &ElementRc, prop_name: &str) {
elem.borrow()
.property_analysis
.borrow_mut()
.entry(prop_name.into())
.or_default()
.is_linked_to_read_only = true;
}
}
pub fn resolve_two_way_binding(
node: syntax_nodes::TwoWayBinding,
ctx: &mut LookupCtx,
) -> Option<NamedReference> {
let Some(n) = node.Expression().QualifiedName() else {
ctx.diag.push_error(
"The expression in a two way binding must be a property reference".into(),
&node.Expression(),
);
return None;
};
let Some(r) = lookup_qualified_name_node(n, ctx, LookupPhase::ResolvingTwoWayBindings) else {
assert!(ctx.diag.has_errors());
return None;
};
// If type is invalid, error has already been reported, when inferring, the error will be reported by the inferring code
let report_error = !matches!(
ctx.property_type,
Type::InferredProperty | Type::InferredCallback | Type::Invalid
);
match r {
LookupResult::Expression { expression: Expression::PropertyReference(n), .. } => {
if report_error && n.ty() != ctx.property_type {
ctx.diag.push_error(
"The property does not have the same type as the bound property".into(),
&node,
);
}
Some(n)
}
LookupResult::Callable(LookupResultCallable::Callable(Callable::Callback(n))) => {
if report_error && n.ty() != ctx.property_type {
ctx.diag.push_error("Cannot bind to a callback".into(), &node);
None
} else {
Some(n)
}
}
LookupResult::Callable(..) => {
if report_error {
ctx.diag.push_error("Cannot bind to a function".into(), &node);
}
None
}
_ => {
ctx.diag.push_error(
"The expression in a two way binding must be a property reference".into(),
&node,
);
None
}
}
}