use super::*;
pub(super) const EXPRESSION_FUNCTIONS: &[&str] = &[
"bucket",
"cidr",
"contains",
"endsWith",
"glob",
"has",
"matches",
"missing",
"path",
"present",
"semver",
"size",
"startsWith",
"timeAfter",
"timeAtOrAfter",
"timeAtOrBefore",
"timeBefore",
"timeBetween",
];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ExpressionOperator {
And,
Or,
Equals,
NotEquals,
Less,
LessEquals,
Greater,
GreaterEquals,
In,
}
pub(super) const LOGICAL_OPERATORS: &[ExpressionOperator] =
&[ExpressionOperator::And, ExpressionOperator::Or];
pub(super) const COMPARISON_OPERATORS: &[ExpressionOperator] = &[
ExpressionOperator::Equals,
ExpressionOperator::NotEquals,
ExpressionOperator::Less,
ExpressionOperator::LessEquals,
ExpressionOperator::Greater,
ExpressionOperator::GreaterEquals,
ExpressionOperator::In,
];
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum ExpressionKey {
When,
Query,
}
pub(super) struct ExpressionCursor {
pub(super) key: ExpressionKey,
pub(super) prefix: String,
pub(super) token: String,
}
pub(super) fn expression_completion_items(
snapshot: &PackageLintSnapshot,
path: &str,
position: SourcePosition,
) -> Option<Vec<PackageCompletionItem>> {
let source_kind = document_kind(snapshot, path)?;
if !matches!(source_kind, SourceKind::Variable) {
return None;
}
let cursor = expression_cursor_at_position(snapshot, path, position)?;
let token_range =
|token: &str| single_line_replace_range(position, token.encode_utf16().count());
if variable_reference_prefix(&cursor.prefix).is_some() {
let mut items = variable_completion_items(&snapshot.index);
stamp_replace_range(&mut items, token_range(&cursor.token));
return Some(items);
}
if cursor.token.starts_with("env.") {
let mut items = env_member_completion_items();
stamp_replace_range(&mut items, token_range(&cursor.token));
return Some(items);
}
if cursor.token.starts_with("lists.") {
let mut items = list_reference_completion_items(&snapshot.index);
stamp_replace_range(&mut items, token_range(&cursor.token));
return Some(items);
}
if cursor.token.starts_with("context.") {
let mut items = context_path_completion_items(snapshot, &cursor.token);
stamp_replace_range(&mut items, token_range(&cursor.token));
return Some(items);
}
if cursor.key == ExpressionKey::Query && cursor.token.starts_with("entry.") {
let mut items = entry_path_completion_items(snapshot, path, &cursor.token);
stamp_replace_range(&mut items, token_range(&cursor.token));
return Some(items);
}
match expression_completion_state(&cursor.prefix) {
ExpressionCompletionState::Operand => {
let mut items = call_argument_completion_items(&snapshot.index, &cursor.prefix);
items.extend(typed_operand_completion_items(snapshot, path, &cursor));
items.extend(expression_root_completion_items(
cursor.key == ExpressionKey::Query,
));
items.extend(expression_function_completion_items());
stamp_replace_range(&mut items, token_range(&cursor.token));
Some(items)
}
ExpressionCompletionState::Operators(operators) => {
let mut items = expression_operator_completion_items(&operators);
stamp_replace_range(
&mut items,
token_range(trailing_operator_token(&cursor.prefix)),
);
Some(items)
}
}
}
pub(super) enum ExpressionCompletionState {
Operand,
Operators(Vec<ExpressionOperator>),
}
pub(super) fn expression_completion_state(raw_prefix: &str) -> ExpressionCompletionState {
let prefix = raw_prefix.trim_end();
if prefix.is_empty() || expression_prefix_expects_operand(prefix) {
return ExpressionCompletionState::Operand;
}
if raw_prefix.ends_with(|ch: char| ch.is_ascii_alphanumeric() || ch == '_') {
return ExpressionCompletionState::Operand;
}
if let Some(operator) = partial_logical_operator_completion(prefix) {
return ExpressionCompletionState::Operators(vec![operator]);
}
match Expression::parse(prefix) {
Ok(expression) => match expression.result_hint() {
ExpressionResultHint::Bool => {
ExpressionCompletionState::Operators(LOGICAL_OPERATORS.to_vec())
}
ExpressionResultHint::Value => {
ExpressionCompletionState::Operators(COMPARISON_OPERATORS.to_vec())
}
},
Err(_) => ExpressionCompletionState::Operand,
}
}
pub(super) fn partial_logical_operator_completion(prefix: &str) -> Option<ExpressionOperator> {
let (candidate, operator) = if let Some(candidate) = prefix.strip_suffix('&') {
(candidate, ExpressionOperator::And)
} else {
let candidate = prefix.strip_suffix('|')?;
(candidate, ExpressionOperator::Or)
};
expression_prefix_is_boolean(candidate.trim_end()).then_some(operator)
}
pub(super) fn expression_prefix_is_boolean(prefix: &str) -> bool {
if prefix.is_empty() {
return false;
}
Expression::parse(prefix)
.is_ok_and(|expression| expression.result_hint() == ExpressionResultHint::Bool)
}
pub(super) fn expression_prefix_expects_operand(prefix: &str) -> bool {
prefix.ends_with("&&")
|| prefix.ends_with("||")
|| prefix.ends_with("==")
|| prefix.ends_with("!=")
|| prefix.ends_with("<=")
|| prefix.ends_with(">=")
|| prefix.ends_with('<')
|| prefix.ends_with('>')
|| prefix.ends_with('!')
|| prefix.ends_with('(')
|| prefix.ends_with('[')
|| prefix.ends_with(',')
|| expression_ends_with_word_operator(prefix, "in")
}
pub(super) fn expression_ends_with_word_operator(prefix: &str, operator: &str) -> bool {
let Some(candidate) = prefix.strip_suffix(operator) else {
return false;
};
candidate
.chars()
.next_back()
.is_none_or(|ch| !is_expression_token_char(ch))
}
pub(super) fn expression_root_completion_items(include_entry: bool) -> Vec<PackageCompletionItem> {
let mut roots = vec!["context.", "env.", "lists."];
if include_entry {
roots.push("entry.");
}
roots
.into_iter()
.map(|root| {
PackageCompletionItem::new(
root,
PackageCompletionItemKind::FieldSelector,
"expression root",
)
})
.collect()
}
pub(super) fn list_reference_completion_items(
index: &crate::lint::index::SemanticIndex,
) -> Vec<PackageCompletionItem> {
index
.lists
.keys()
.map(|id| {
let label = if id.contains('/') {
format!("lists[\"{id}\"]")
} else {
format!("lists.{id}")
};
PackageCompletionItem::new(
label,
PackageCompletionItemKind::FieldSelector,
"list members",
)
})
.collect()
}
pub(super) fn env_member_completion_items() -> Vec<PackageCompletionItem> {
["env.now"]
.into_iter()
.map(|member| {
PackageCompletionItem::new(
member,
PackageCompletionItemKind::FieldSelector,
"env member",
)
})
.collect()
}
pub(super) fn expression_function_completion_items() -> Vec<PackageCompletionItem> {
EXPRESSION_FUNCTIONS
.iter()
.copied()
.map(|function| {
PackageCompletionItem::new(
format!("{function}("),
PackageCompletionItemKind::Function,
"expression function",
)
})
.collect()
}
pub(super) fn expression_operator_completion_items(
operators: &[ExpressionOperator],
) -> Vec<PackageCompletionItem> {
operators
.iter()
.map(|operator| {
let label = match operator {
ExpressionOperator::And => "&&",
ExpressionOperator::Or => "||",
ExpressionOperator::Equals => "==",
ExpressionOperator::NotEquals => "!=",
ExpressionOperator::Less => "<",
ExpressionOperator::LessEquals => "<=",
ExpressionOperator::Greater => ">",
ExpressionOperator::GreaterEquals => ">=",
ExpressionOperator::In => "in",
};
PackageCompletionItem::new(
label,
PackageCompletionItemKind::Operator,
"expression operator",
)
.with_insert_text(format!("{label} "))
})
.collect()
}
pub(super) fn variable_expression_at_position(
index: &SemanticIndex,
path: &str,
position: SourcePosition,
) -> bool {
let Some(variable) = current_variable_for_path(index, path) else {
return false;
};
let ResolveNode::Resolve { rules, query, .. } = &variable.resolve else {
return false;
};
if let Some(query) = query
&& [&query.filter, &query.sort]
.into_iter()
.flatten()
.any(|field| location_contains_position(&field.location(), path, position))
{
return true;
}
let RuleCollection::Rules(rules) = rules else {
return false;
};
rules.iter().any(|rule| {
rule.when
.as_ref()
.is_some_and(|field| location_contains_position(&field.location(), path, position))
})
}